diff --git a/.gitattributes b/.gitattributes index 1359bb5fc..ccc76301f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,14 @@ # ensures this even if the user has not set core.autocrlf. * text=auto +# Checkout JavaScript files with LF line endings +# to prevent `prettier` from reporting errors on Windows. +*.js eol=lf +*.jsx eol=lf +*.ts eol=lf +*.tsx eol=lf +*.json eol=lf + # This directory contains email messages verbatim, and changing CRLF to # LF will corrupt them. test-data/** text=false diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4225c7453..9623db596 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,5 +5,5 @@ updates: schedule: interval: "monthly" commit-message: - prefix: "cargo" + prefix: "chore(cargo)" open-pull-requests-limit: 50 diff --git a/.github/mergeable.yml b/.github/mergeable.yml deleted file mode 100644 index 9eeb3615c..000000000 --- a/.github/mergeable.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: 2 -mergeable: - - when: pull_request.* - name: "Changelog check" - validate: - - do: or - validate: - - do: description - must_include: - regex: "#skip-changelog" - - do: and - validate: - - do: dependent - changed: - file: "src/**" - required: ["CHANGELOG.md"] - - do: dependent - changed: - file: "deltachat-ffi/src/**" - required: ["CHANGELOG.md"] - fail: - - do: checks - status: "action_required" - payload: - title: Changelog might need an update - summary: "Check if CHANGELOG.md needs an update or add #skip-changelog to the PR description." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0659c359..dfef71fce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,3 +1,7 @@ +# GitHub Actions workflow to +# lint Rust and Python code +# and run Rust tests, Python tests and async Python tests. + name: Rust CI # Cancel previously started workflow runs @@ -11,6 +15,7 @@ on: push: branches: - master + - stable env: RUSTFLAGS: -Dwarnings @@ -20,7 +25,7 @@ jobs: name: Lint Rust runs-on: ubuntu-latest env: - RUSTUP_TOOLCHAIN: 1.68.2 + RUSTUP_TOOLCHAIN: 1.73.0 steps: - uses: actions/checkout@v3 - name: Install rustfmt and clippy @@ -72,19 +77,15 @@ jobs: matrix: include: - os: ubuntu-latest - rust: 1.68.2 + rust: 1.73.0 - os: windows-latest - rust: 1.68.2 + rust: 1.73.0 - os: macos-latest - rust: 1.68.2 + rust: 1.73.0 - # Minimum Supported Rust Version = 1.65.0 - # - # Minimum Supported Python Version = 3.7 - # This is the minimum version for which manylinux Python wheels are - # built. + # Minimum Supported Rust Version = 1.67.0 - os: ubuntu-latest - rust: 1.65.0 + rust: 1.67.0 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 @@ -97,6 +98,8 @@ jobs: uses: swatinem/rust-cache@v2 - name: Tests + env: + RUST_BACKTRACE: 1 run: cargo test --workspace - name: Test cargo vendor @@ -173,15 +176,15 @@ jobs: include: # Currently used Rust version. - os: ubuntu-latest - python: 3.11 + python: 3.12 - os: macos-latest - python: 3.11 + python: 3.12 # PyPy tests - os: ubuntu-latest - python: pypy3.9 + python: pypy3.10 - os: macos-latest - python: pypy3.9 + python: pypy3.10 # Minimum Supported Python Version = 3.7 # This is the minimum version for which manylinux Python wheels are @@ -222,21 +225,18 @@ jobs: fail-fast: false matrix: include: - # Currently used Rust version. - os: ubuntu-latest - python: 3.11 + python: 3.12 - os: macos-latest - python: 3.11 + python: 3.12 # PyPy tests - os: ubuntu-latest - python: pypy3.9 + python: pypy3.10 - os: macos-latest - python: pypy3.9 + python: pypy3.10 # Minimum Supported Python Version = 3.7 - # This is the minimum version for which manylinux Python wheels are - # built. Test it with minimum supported Rust version. - os: ubuntu-latest python: 3.7 diff --git a/.github/workflows/deltachat-rpc-server.yml b/.github/workflows/deltachat-rpc-server.yml index bba139e30..4abd9891a 100644 --- a/.github/workflows/deltachat-rpc-server.yml +++ b/.github/workflows/deltachat-rpc-server.yml @@ -1,4 +1,10 @@ -# Manually triggered action to build deltachat-rpc-server binaries. +# GitHub Actions workflow +# to build `deltachat-rpc-server` binaries +# and upload them to the release. +# +# The workflow is automatically triggered on releases. +# It can also be triggered manually +# to produce binary artifacts for testing. name: Build deltachat-rpc-server binaries @@ -20,35 +26,17 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Build + - name: Install ziglang + run: pip install wheel ziglang==0.11.0 + + - name: Build deltachat-rpc-server binaries run: sh scripts/zig-rpc-server.sh - - name: Upload x86_64 binary + - name: Upload dist directory with Linux binaries uses: actions/upload-artifact@v3 with: - name: deltachat-rpc-server-x86_64 - path: target/x86_64-unknown-linux-musl/release/deltachat-rpc-server - if-no-files-found: error - - - name: Upload i686 binary - uses: actions/upload-artifact@v3 - with: - name: deltachat-rpc-server-i686 - path: target/i686-unknown-linux-musl/release/deltachat-rpc-server - if-no-files-found: error - - - name: Upload aarch64 binary - uses: actions/upload-artifact@v3 - with: - name: deltachat-rpc-server-aarch64 - path: target/aarch64-unknown-linux-musl/release/deltachat-rpc-server - if-no-files-found: error - - - name: Upload armv7 binary - uses: actions/upload-artifact@v3 - with: - name: deltachat-rpc-server-armv7 - path: target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server + name: linux + path: dist/ if-no-files-found: error build_windows: @@ -84,22 +72,89 @@ jobs: path: target/${{ matrix.target}}/release/${{ matrix.path }} if-no-files-found: error + build_macos: + name: Build deltachat-rpc-server for macOS + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + - arch: aarch64 + + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup rust target + run: rustup target add ${{ matrix.arch }}-apple-darwin + + - name: Build + run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored + + - name: Upload binary + uses: actions/upload-artifact@v3 + with: + name: deltachat-rpc-server-${{ matrix.arch }}-macos + path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server + if-no-files-found: error + publish: - name: Upload binaries to the release - needs: ["build_linux", "build_windows"] + name: Build wheels and upload binaries to the release + needs: ["build_linux", "build_windows", "build_macos"] permissions: contents: write runs-on: "ubuntu-latest" steps: - - name: Download built binaries - uses: "actions/download-artifact@v3" + - uses: actions/checkout@v3 - - name: Compose dist/ directory + - name: Download Linux binaries + uses: actions/download-artifact@v3 + with: + name: linux + path: dist/ + + - name: Download win32 binary + uses: actions/download-artifact@v3 + with: + name: deltachat-rpc-server-win32.exe + path: deltachat-rpc-server-win32.exe.d + + - name: Download win64 binary + uses: actions/download-artifact@v3 + with: + name: deltachat-rpc-server-win64.exe + path: deltachat-rpc-server-win64.exe.d + + - name: Download macOS binary for x86_64 + uses: actions/download-artifact@v3 + with: + name: deltachat-rpc-server-x86_64-macos + path: deltachat-rpc-server-x86_64-macos.d + + - name: Download macOS binary for aarch64 + uses: actions/download-artifact@v3 + with: + name: deltachat-rpc-server-aarch64-macos + path: deltachat-rpc-server-aarch64-macos.d + + - name: Flatten dist/ directory run: | - mkdir dist - for x in x86_64 i686 aarch64 armv7 win32.exe win64.exe; do - mv "deltachat-rpc-server-$x"/* "dist/deltachat-rpc-server-$x" - done + mv deltachat-rpc-server-win32.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win32.exe + mv deltachat-rpc-server-win64.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win64.exe + mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-macos + mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-macos + + # Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py + - name: Install python 3.12 + uses: actions/setup-python@v4 + with: + python-version: 3.12 + + - name: Install wheel + run: pip install wheel + + - name: Build deltachat-rpc-server Python wheels and source package + run: scripts/wheel-rpc-server.py - name: List downloaded artifacts run: ls -l dist/ diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 174f6096e..3c24454fa 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -1,3 +1,6 @@ +# GitHub Actions workflow +# to automatically approve PRs made by Dependabot. + name: Dependabot auto-approve on: pull_request diff --git a/.github/workflows/jsonrpc-client-npm-package.yml b/.github/workflows/jsonrpc-client-npm-package.yml index 57ec35095..8d371c6d6 100644 --- a/.github/workflows/jsonrpc-client-npm-package.yml +++ b/.github/workflows/jsonrpc-client-npm-package.yml @@ -38,13 +38,12 @@ jobs: node --version echo $DELTACHAT_JSONRPC_TAR_GZ - name: Install dependencies without running scripts - run: | - cd deltachat-jsonrpc/typescript - npm install --ignore-scripts + working-directory: deltachat-jsonrpc/typescript + run: npm install --ignore-scripts - name: Package shell: bash + working-directory: deltachat-jsonrpc/typescript run: | - cd deltachat-jsonrpc/typescript npm run build npm pack . ls -lah diff --git a/.github/workflows/jsonrpc.yml b/.github/workflows/jsonrpc.yml index 692150cd7..5f55ab529 100644 --- a/.github/workflows/jsonrpc.yml +++ b/.github/workflows/jsonrpc.yml @@ -22,24 +22,19 @@ jobs: - name: Add Rust cache uses: Swatinem/rust-cache@v2 - name: npm install - run: | - cd deltachat-jsonrpc/typescript - npm install + working-directory: deltachat-jsonrpc/typescript + run: npm install - name: Build TypeScript, run Rust tests, generate bindings - run: | - cd deltachat-jsonrpc/typescript - npm run build + working-directory: deltachat-jsonrpc/typescript + run: npm run build - name: Run integration tests - run: | - cd deltachat-jsonrpc/typescript - npm run test + working-directory: deltachat-jsonrpc/typescript + run: npm run test env: DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }} - name: make sure websocket server version still builds - run: | - cd deltachat-jsonrpc - cargo build --bin deltachat-jsonrpc-server --features webserver + working-directory: deltachat-jsonrpc + run: cargo build --bin deltachat-jsonrpc-server --features webserver - name: Run linter - run: | - cd deltachat-jsonrpc/typescript - npm run prettier:check + working-directory: deltachat-jsonrpc/typescript + run: npm run prettier:check diff --git a/.github/workflows/node-delete-preview.yml b/.github/workflows/node-delete-preview.yml deleted file mode 100644 index 34ba9477f..000000000 --- a/.github/workflows/node-delete-preview.yml +++ /dev/null @@ -1,31 +0,0 @@ -# documentation: https://github.com/deltachat/sysadmin/tree/master/download.delta.chat -name: Delete node PR previews - -on: - pull_request: - types: [closed] - -jobs: - delete: - runs-on: ubuntu-latest - - steps: - - name: Get Pull Request ID - id: getid - run: | - export PULLREQUEST_ID=$(jq .number < $GITHUB_EVENT_PATH) - echo "prid=$PULLREQUEST_ID" >> $GITHUB_OUTPUT - - name: Renaming - run: | - # create empty file to copy it over the outdated deliverable on download.delta.chat - echo "This preview build is outdated and has been removed." > empty - cp empty deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz - - name: Replace builds with dummy files - uses: horochx/deploy-via-scp@v1.0.1 - with: - user: ${{ secrets.USERNAME }} - key: ${{ secrets.SSH_KEY }} - host: "download.delta.chat" - port: 22 - local: "deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz" - remote: "/var/www/html/download/node/preview/" diff --git a/.github/workflows/node-docs.yml b/.github/workflows/node-docs.yml index 1cd1a60d4..86ff68eee 100644 --- a/.github/workflows/node-docs.yml +++ b/.github/workflows/node-docs.yml @@ -1,3 +1,8 @@ +# GitHub Actions workflow to build +# Node.js bindings documentation +# and upload it to the web server. +# Built documentation is available at + name: Generate & upload node.js documentation on: @@ -17,8 +22,8 @@ jobs: node-version: 16.x - name: npm install and generate documentation + working-directory: node run: | - cd node npm i --ignore-scripts npx typedoc mv docs js diff --git a/.github/workflows/node-package.yml b/.github/workflows/node-package.yml index 7575de84d..5e5b79108 100644 --- a/.github/workflows/node-package.yml +++ b/.github/workflows/node-package.yml @@ -12,7 +12,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-20.04, macos-latest, windows-latest] + os: [macos-latest, windows-latest] steps: - name: Checkout uses: actions/checkout@v3 @@ -46,13 +46,12 @@ jobs: - name: Install dependencies & build if: steps.cache.outputs.cache-hit != 'true' - run: | - cd node - npm install --verbose + working-directory: node + run: npm install --verbose - name: Build Prebuild + working-directory: node run: | - cd node npm run prebuildify tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds . @@ -62,10 +61,81 @@ jobs: name: ${{ matrix.os }} path: node/${{ matrix.os }}.tar.gz + prebuild-linux: + name: Prebuild Linux + runs-on: ubuntu-latest + + # Build Linux prebuilds inside a container with old glibc for backwards compatibility. + # Debian 10 contained glibc 2.28 at the time of the writing (2023-06-04): https://packages.debian.org/buster/libc6 + # Ubuntu 18.04 is at the End of Standard Support since June 2023, but it contains glibc 2.27, + # so we are using it to support Ubuntu 18.04 setups that are still not upgraded. + container: ubuntu:18.04 + steps: + # Working directory is owned by 1001:1001 by default. + # Change it to our user. + - name: Change working directory owner + run: chown root:root . + + - name: Checkout + uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: "16" + - run: apt-get update + + # Python is needed for node-gyp + - name: Install curl, python and compilers + run: apt-get install -y curl build-essential python3 + - name: Install Rust + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + - name: System info + run: | + rustc -vV + rustup -vV + cargo -vV + npm --version + node --version + + - name: Cache node modules + uses: actions/cache@v3 + with: + path: | + ${{ env.APPDATA }}/npm-cache + ~/.npm + key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }} + + - name: Cache cargo index + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/ + ~/.cargo/git + target + key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2 + + - name: Install dependencies & build + if: steps.cache.outputs.cache-hit != 'true' + working-directory: node + run: npm install --verbose + + - name: Build Prebuild + working-directory: node + run: | + npm run prebuildify + tar -zcvf "linux.tar.gz" -C prebuilds . + + - name: Upload Prebuild + uses: actions/upload-artifact@v3 + with: + name: linux + path: node/linux.tar.gz + pack-module: - needs: prebuild + needs: [prebuild, prebuild-linux] name: Package deltachat-node and upload to download.delta.chat - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: Install tree run: sudo apt install tree @@ -96,10 +166,10 @@ jobs: npm --version node --version echo $DELTACHAT_NODE_TAR_GZ - - name: Download Ubuntu prebuild + - name: Download Linux prebuild uses: actions/download-artifact@v1 with: - name: ubuntu-20.04 + name: linux - name: Download macOS prebuild uses: actions/download-artifact@v1 with: @@ -111,11 +181,11 @@ jobs: - shell: bash run: | mkdir node/prebuilds - tar -xvzf ubuntu-20.04/ubuntu-20.04.tar.gz -C node/prebuilds + tar -xvzf linux/linux.tar.gz -C node/prebuilds tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds tree node/prebuilds - rm -rf ubuntu-20.04 macos-latest windows-latest + rm -rf linux macos-latest windows-latest - name: Install dependencies without running scripts run: | npm install --ignore-scripts diff --git a/.github/workflows/node-tests.yml b/.github/workflows/node-tests.yml index 62ab1d0fd..6581be005 100644 --- a/.github/workflows/node-tests.yml +++ b/.github/workflows/node-tests.yml @@ -1,3 +1,6 @@ +# GitHub Actions workflow +# to test Node.js bindings. + name: "node.js tests" # Cancel previously started workflow runs @@ -52,25 +55,13 @@ jobs: - name: Install dependencies & build if: steps.cache.outputs.cache-hit != 'true' - run: | - cd node - npm install --verbose + working-directory: node + run: npm install --verbose - name: Test timeout-minutes: 10 - if: runner.os != 'Windows' - run: | - cd node - npm run test - env: - DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }} - NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true" - - name: Run tests on Windows, except lint - timeout-minutes: 10 - if: runner.os == 'Windows' - run: | - cd node - npm run test:mocha + working-directory: node + run: npm run test env: DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }} NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true" diff --git a/.github/workflows/repl.yml b/.github/workflows/repl.yml index a3d391fe2..ee71ef2f9 100644 --- a/.github/workflows/repl.yml +++ b/.github/workflows/repl.yml @@ -1,4 +1,5 @@ -# Manually triggered action to build a Windows repl.exe which users can +# Manually triggered GitHub Actions workflow +# to build a Windows repl.exe which users can # download to debug complex bugs. name: Build Windows REPL .exe diff --git a/.github/workflows/upload-ffi-docs.yml b/.github/workflows/upload-ffi-docs.yml index c230b715e..9acca2f98 100644 --- a/.github/workflows/upload-ffi-docs.yml +++ b/.github/workflows/upload-ffi-docs.yml @@ -1,3 +1,7 @@ +# GitHub Actions workflow +# to build `deltachat_fii` crate documentation +# and upload it to + name: Build & Deploy Documentation on cffi.delta.chat on: diff --git a/.gitignore b/.gitignore index ff914ab6b..aed04dd95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /target **/*.rs.bk /build +/dist # ignore vi temporaries *~ @@ -18,6 +19,9 @@ python/.eggs __pycache__ python/src/deltachat/capi*.so python/.venv/ +python/venv/ +venv/ +env/ python/liveconfig* diff --git a/CHANGELOG.md b/CHANGELOG.md index 21a540326..9ca15c6da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,33 +1,579 @@ # Changelog -## [Unreleased] +## [1.126.1] - 2023-10-24 + +### Fixes + +- Do not hardcode version in deltachat-rpc-server source package. +- Do not interrupt IMAP loop from `get_connectivity_html()`. + +### Features / Changes + +- imap: Buffer `STARTTLS` command. + +### Build system + +- Build `deltachat-rpc-server` binary for aarch64 macOS. +- Build `deltachat-rpc-server` wheels for macOS and Windows. + +### Refactor + +- Remove job queue. + +### Miscellaneous Tasks + +- cargo: Update `ahash` to make `cargo-deny` happy. + +## [1.126.0] - 2023-10-22 + +### API-Changes + +- Allow to filter by unread in `chatlist:try_load` ([#4824](https://github.com/deltachat/deltachat-core-rust/pull/4824)). +- Add `misc_send_draft()` to JSON-RPC API ([#4839](https://github.com/deltachat/deltachat-core-rust/pull/4839)). + +### Features / Changes + +- [**breaking**] Make broadcast lists create their own chat ([#4644](https://github.com/deltachat/deltachat-core-rust/pull/4644)). + - This means that UIs need to ask for the name when creating a broadcast list, similar to . +- Add self-address to backup filename ([#4820](https://github.com/deltachat/deltachat-core-rust/pull/4820)) + +### CI + +- Build Python wheels for deltachat-rpc-server. + +### Build system + +- Strip release binaries. +- Workaround OpenSSL crate expecting libatomic to be available. + +### Fixes + +- Set `soft_heap_limit` on SQLite database. +- imap: Fallback to `STATUS` if `SELECT` did not return UIDNEXT. + +## [1.125.0] - 2023-10-14 + +### API-Changes + +- [**breaking**] deltachat-rpc-client: Replace `asyncio` with threads. +- Validate boolean values passed to `set_config`. Attempts to set values other than `0` and `1` will result in an error. + +### CI + +- Reduce required Python version for deltachat-rpc-client from 3.8 to 3.7. + +### Features / Changes + +- Add developer option to disable IDLE. + +### Fixes + +- `deltachat-rpc-client`: Run `deltachat-rpc-server` in its own process group. This prevents reception of `SIGINT` by the server when the bot is terminated with `^C`. +- python: Don't automatically set the displayname to "bot" when setting log level. +- Don't update `timestamp`, `timestamp_rcvd`, `state` when replacing partially downloaded message ([#4700](https://github.com/deltachat/deltachat-core-rust/pull/4700)). +- Assign encrypted partially downloaded group messages to 1:1 chat ([#4757](https://github.com/deltachat/deltachat-core-rust/pull/4757)). +- Return all contacts from `Contact::get_all` for bots ([#4811](https://github.com/deltachat/deltachat-core-rust/pull/4811)). +- Set connectivity status to "connected" during fake idle. +- Return verifier contacts regardless of their origin. +- Don't try to send more MDNs if there's a temporary SMTP error ([#4534](https://github.com/deltachat/deltachat-core-rust/pull/4534)). + +### Refactor + +- deltachat-rpc-client: Close stdin instead of sending `SIGTERM`. +- deltachat-rpc-client: Remove print() calls. Standard `logging` package is for logging instead. + +### Tests + +- deltachat-rpc-client: Enable logs in pytest. + +## [1.124.1] - 2023-10-05 + +### Fixes + +- Remove footer from reactions on the receiver side ([#4780](https://github.com/deltachat/deltachat-core-rust/pull/4780)). + +### CI + +- Pin `urllib3` version to `<2`. ([#4788](https://github.com/deltachat/deltachat-core-rust/issues/4788)) + +## [1.124.0] - 2023-10-04 + +### API-Changes + +- [**breaking**] Return `DC_CONTACT_ID_SELF` from `dc_contact_get_verifier_id()` for directly verified contacts. +- Deprecate `dc_contact_get_verifier_addr`. +- python: use `dc_contact_get_verifier_id()`. `get_verifier()` returns a Contact rather than an address now. +- Deprecate `get_next_media()`. +- Ignore public key argument in `dc_preconfigure_keypair()`. Public key is extracted from the private key. + +### Fixes + +- Wrap base64-encoded parts to 76 characters. +- Require valid email addresses in `dc_provider_new_from_email[_with_dns]`. +- Do not trash messages with attachments and no text when `location.kml` is attached ([#4749](https://github.com/deltachat/deltachat-core-rust/issues/4749)). +- Initialise `last_msg_id` to the highest known row id. This ensures bots migrated from older version to `dc_get_next_msgs()` API do not process all previous messages from scratch. +- Do not put the status footer into reaction MIME parts. +- Ignore special chats in `get_similar_chat_ids()`. This prevents trash chat from showing up in similar chat list ([#4756](https://github.com/deltachat/deltachat-core-rust/issues/4756)). +- Cap percentage in connectivity layout to 100% ([#4765](https://github.com/deltachat/deltachat-core-rust/pull/4765)). +- Add Let's Encrypt root certificate to `reqwest`. This should allow scanning `DCACCOUNT` QR-codes on older Android phones when the server has a Let's Encrypt certificate. +- deltachat-rpc-client: Increase stdio buffer to 64 MiB to avoid Python bots crashing when trying to load large messages via a JSON-RPC call. +- Add `protected-headers` directive to Content-Type of encrypted messages with attachments ([#2302](https://github.com/deltachat/deltachat-core-rust/issues/2302)). This makes Thunderbird show encrypted Subject for Delta Chat messages. +- webxdc: Reset `document.update` on forwarding. This fixes the test `test_forward_webxdc_instance()`. + +### Features / Changes + +- Remove extra members from the local list in sake of group membership consistency ([#3782](https://github.com/deltachat/deltachat-core-rust/issues/3782)). +- deltachat-rpc-client: Log exceptions when long-running tasks die. + +### Build + +- Build wheels for Python 3.12 and PyPy 3.10. + +## [1.123.0] - 2023-09-22 + +### API-Changes + +- Make it possible to import secret key from a file with `DC_IMEX_IMPORT_SELF_KEYS`. +- [**breaking**] Make `dc_jsonrpc_blocking_call` accept JSON-RPC request. + +### Fixes + +- `lookup_chat_by_reply()`: Skip not fully downloaded and undecipherable messages ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)). +- `lookup_chat_by_reply()`: Skip undecipherable parent messages created by older versions ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)). +- imex: Use "default" in the filename of the default key. + +### Miscellaneous Tasks + +- Update OpenSSL from 3.1.2 to 3.1.3. + +## [1.122.0] - 2023-09-12 + +### API-Changes + +- jsonrpc: Return only chat IDs for similar chats. + +### Fixes + +- Reopen all connections on database passpharse change. +- Do not block new group chats if 1:1 chat is blocked. +- Improve group membership consistency algorithm ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782))([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)). +- Forbid membership changes from possible non-members ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782)). +- `ChatId::parent_query()`: Don't filter out OutPending and OutFailed messages. + +### Build system + +- Update to OpenSSL 3.0. +- Bump webpki from 0.22.0 to 0.22.1. +- python: Add link to Mastodon into projects.urls. + +### Features / Changes + +- Add RSA-4096 key generation support. + +### Refactor + +- pgp: Add constants for encryption algorithm and hash. + +## [1.121.0] - 2023-09-06 + +### API-Changes + +- Add `dc_context_change_passphrase()`. +- Add `Message.set_file_from_bytes()` API. +- Add experimental API to get similar chats. + +### Build system + +- Build node packages on Ubuntu 18.04 instead of Debian 10. + This reduces the requirement for glibc version from 2.28 to 2.27. + +### Fixes + +- Allow membership changes by a MUA if we're not in the group ([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)). +- Save mime headers for messages not signed with a known key ([#4557](https://github.com/deltachat/deltachat-core-rust/pull/4557)). +- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/deltachat/deltachat-core-rust/pull/4616)). +- Do not allow dots at the end of email addresses. +- deltachat-rpc-client: Remove `aiodns` optional dependency from required dependencies. + `aiodns` depends on `pycares` which [fails to install in Termux](https://github.com/saghul/aiodns/issues/98). + +## [1.120.0] - 2023-08-28 + +### API-Changes + +- jsonrpc: Add `resend_messages`. + +### Fixes + +- Update async-imap to 0.9.1 to fix memory leak. +- Delete messages from SMTP queue only on user demand ([#4579](https://github.com/deltachat/deltachat-core-rust/pull/4579)). +- Do not send images without transparency as stickers ([#4611](https://github.com/deltachat/deltachat-core-rust/pull/4611)). +- `prepare_msg_blob()`: do not use the image if it has Exif metadata but the image cannot be recoded. + +### Refactor + +- Hide accounts.rs constants from public API. +- Hide pgp module from public API. + +### Build system + +- Update to Zig 0.11.0. +- Update to Rust 1.72.0. + +### CI + +- Run on push to stable branch. + +### Miscellaneous Tasks + +- python: Fix lint errors. +- python: Fix `ruff` 0.0.286 warnings. +- Fix beta clippy warnings. + +## [1.119.1] - 2023-08-06 + +Bugfix release attempting to fix the [iOS build error](https://github.com/deltachat/deltachat-core-rust/issues/4610). + +### Features / Changes + +- Guess message viewtype from "application/octet-stream" attachment extension ([#4378](https://github.com/deltachat/deltachat-core-rust/pull/4378)). + +### Fixes + +- Update `xattr` from 1.0.0 to 1.0.1 to fix UnsupportedPlatformError import. + +### Tests + +- webxdc: Ensure unknown WebXDC update properties do not result in an error. + +## [1.119.0] - 2023-08-03 + +### Fixes + +- imap: Avoid IMAP move loops when DeltaChat folder is aliased. +- imap: Do not resync IMAP after initial configuration. + +- webxdc: Accept WebXDC updates in mailing lists. +- webxdc: Base64-encode WebXDC updates to prevent corruption of large unencrypted WebXDC updates. +- webxdc: Delete old webxdc status updates during housekeeping. + +- Return valid MsgId from `receive_imf()` when the message is replaced. +- Emit MsgsChanged event with correct chat id for replaced messages. + +- deltachat-rpc-server: Update tokio-tar to fix backup import. + +### Features / Changes + +- deltachat-rpc-client: Add `MSG_DELETED` constant. +- Make `dc_msg_get_filename()` return the original attachment filename ([#4309](https://github.com/deltachat/deltachat-core-rust/pull/4309)). + +### API-Changes + +- deltachat-rpc-client: Add `Account.{import,export}_backup` methods. +- deltachat-jsonrpc: Make `MessageObject.text` non-optional. + +### Documentation + +- Update default value for `show_emails` in `dc_set_config()` documentation. + +### Refactor + +- Improve IMAP logs. + +### Tests + +- Add basic import/export test for async python. +- Add `test_webxdc_download_on_demand`. +- Add tests for deletion of webxdc status-updates. + +## [1.118.0] - 2023-07-07 + +### API-Changes + +- [**breaking**] Remove `Contact::load_from_db()` in favor of `Contact::get_by_id()`. +- Add `Contact::get_by_id_optional()` API. +- [**breaking**] Make `Message.text` non-optional. +- [**breaking**] Replace `message::get_msg_info()` with `MsgId.get_info()`. +- Move `handle_mdn` and `handle_ndn` to mimeparser and make them private. + Previously `handle_mdn` was erroneously exposed in the public API. +- python: flatten the API of `deltachat` module. + +### Fixes + +- Use different member added/removal messages locally and on the network. +- Update tokio to 1.29.1 to fix core panic after sending 29 offline messages ([#4414](https://github.com/deltachat/deltachat-core-rust/issues/4414)). +- Make SVG avatar image work on more platforms (use `xlink:href`). +- Preserve indentation when converting plaintext to HTML. +- Do not run simplify() on dehtml() output. +- Rewrite member added/removed messages even if the change is not allowed PR ([#4529](https://github.com/deltachat/deltachat-core-rust/pull/4529)). + +### Documentation + +- Document how to regenerate Node.js constants before the release. + +### Build system + +- git-cliff: Do not fail if commit.footers is undefined. + +### Other + +- Dependency updates. +- Update MPL 2.0 license text. +- Add LICENSE file to deltachat-rpc-client. +- deltachat-rpc-client: Add Trove classifiers. +- python: Change bindings status to production/stable. + +### Tests + +- Add `make-python-testenv.sh` script. + +## [1.117.0] - 2023-06-15 + +### Features + +- New group membership update algorithm. + + New algorithm improves group consistency + in cases of missing messages, + restored old backups and replies from classic MUAs. + +- Add `DC_EVENT_MSG_DELETED` event. + + This event notifies the UI about the message + being deleted from the messagelist, e.g. when the message expires + or the user deletes it. + +### Fixes + +- Emit `DC_EVENT_MSGS_CHANGED` without IDs when the message expires. + + Specifying msg IDs that cannot be loaded in the event payload + results in an error when the UI tries to load the message. + Instead, emit an event without IDs + to make the UI reload the whole messagelist. + +- Ignore address case when comparing the `To:` field to `Autocrypt-Gossip:`. + + This bug resulted in failure to propagate verification + if the contact list already contained a new verified group member + with a non-lowercase address. + +- dehtml: skip links with empty text. + + Links like `` in HTML mails are now skipped + instead of being converted to a link without a label like `[](https://delta.chat/)`. + +- dehtml: Do not insert unnecessary newlines when parsing `

` tags. + +- Update from yanked `libc` 0.2.145 to 0.2.146. +- Update to async-imap 0.9.0 to remove deprecated `ouroboros` dependency. + +### API-Changes + +- Emit `DC_EVENT_MSGS_CHANGED` per chat when messages are deleted. + + Previously a single event with zero chat ID was emitted. + +- python: make `Contact.is_verified()` return bool. + +- rust: add API endpoint `get_status_update` ([#4468](https://github.com/deltachat/deltachat-core-rust/pull/4468)). + +- rust: make `WebxdcManifest` type public. + +### Build system + +- Use Rust 1.70.0 to compile deltachat-rpc-server releases. +- Disable unused `brotli` feature `ffi-api` and use 1 codegen-units for release builds to reduce the size of the binaries. + +### CI + +- Run `cargo check` with musl libc. +- concourse: Install devpi in a virtual environment. +- Remove [mergeable](https://mergeable.us/) configuration. + +### Documentation + +- README: mark napi.rs bindings as experimental. CFFI bindings are not legacy and are the recommended Node.js bindings currently. +- CONTRIBUTING: document how conventional commits interact with squash merges. + +### Refactor + +- Rename `MimeMessage.header` into `MimeMessage.headers`. + +- Derive `Default` trait for `WebxdcManifest`. + +### Tests + +- Regression test for case-sensitive comparison of gossip header to contact address. +- Multiple new group consistency tests in Rust. +- python: Replace legacy `tmpdir` fixture with `tmp_path`. + +## [1.116.0] - 2023-06-05 + +### API-Changes + +- Add `dc_jsonrpc_blocking_call()`. + +### Changes + +- Generate OpenRPC definitions for JSON-RPC. +- Add more context to message loading errors. + +### Fixes + +- Build deltachat-node prebuilds on Debian 10. + +### Documentation + +- Document release process in `RELEASE.md`. +- Add contributing guidelines `CONTRIBUTING.md`. +- Update instructions for python devenv. +- python: Document pytest fixtures. + +### Tests + +- python: Make `test_mdn_asymmetric` less flaky. +- Make `test_group_with_removed_message_id` less flaky. +- Add golden tests infrastructure ([#4395](https://github.com/deltachat/deltachat-core-rust/pull/4395)). + +### Build system + +- git-cliff: Changelog generation improvements. +- `set_core_version.py`: Expect release date in the changelog. + +### CI + +- Require Python 3.8 for deltachat-rpc-client. +- mergeable: Allow PR titles to start with "ci" and "build". +- Remove incorrect comment. +- dependabot: Use `chore` prefix for dependency updates. +- Remove broken `node-delete-preview.yml` workflow. +- Add top comments to GH Actions workflows. +- Run node.js lint on Windows. +- Update clippy to 1.70.0. + +### Miscellaneous Tasks + +- Remove release.toml. +- gitattributes: Configure LF line endings for JavaScript files. +- Update dependencies + +## [1.112.10] - 2023-06-01 + +### Fixes + +- Disable `fetch_existing_msgs` setting by default. +- Update `h2` to fix RUSTSEC-2023-0034. + +## [1.115.0] - 2023-05-12 + +### JSON-RPC API Changes + +- Sort reactions in descending order ([#4388](https://github.com/deltachat/deltachat-core-rust/pull/4388)). +- Add API to get reactions outside the message snapshot. +- `get_chatlist_items_by_entries` now takes only chatids instead of `ChatListEntries`. +- `get_chatlist_entries` now returns `Vec` of chatids instead of `ChatListEntries`. +- `JSONRPCReactions.reactions` is now a `Vec` with unique reactions and their count, sorted in descending order. +- `Event`: `context_id` property is now called `contextId`. +- Expand `MessageSearchResult`: + - Always include `chat_name`(not an option anymore). + - Add `author_id`, `chat_type`, `chat_color`, `is_chat_protected`, `is_chat_contact_request`, `is_chat_archived`. + - `author_name` now contains the overridden sender name. +- `ChatListItemFetchResult` gets new properties: `summary_preview_image`, `last_message_type` and `last_message_id` +- New `MessageReadReceipt` type and `get_message_read_receipts(account_id, message_id)` jsonrpc method. + +### API Changes + +- New rust API `send_webxdc_status_update_struct` to send a `StatusUpdateItem`. +- Add `get_msg_read_receipts(context, msg_id)` - get the contacts that send read receipts for a message. + +### Features / Changes + +- Build deltachat-rpc-server releases for x86\_64 macOS. +- Generate changelogs using git-cliff ([#4393](https://github.com/deltachat/deltachat-core-rust/pull/4393), [#4396](https://github.com/deltachat/deltachat-core-rust/pull/4396)). +- Improve SMTP logging. +- Do not cut incoming text if "bot" config is set. + +### Fixes + +- JSON-RPC: typescript client: fix types of events in event emitter ([#4373](https://github.com/deltachat/deltachat-core-rust/pull/4373)). +- Fetch at most 100 existing messages even if EXISTS was not received ([#4383](https://github.com/deltachat/deltachat-core-rust/pull/4383)). +- Don't put a double dot at the end of error messages ([#4398](https://github.com/deltachat/deltachat-core-rust/pull/4398)). +- Recreate `smtp` table with AUTOINCREMENT `id` ([#4390](https://github.com/deltachat/deltachat-core-rust/pull/4390)). +- Do not return an error from `send_msg_to_smtp` if retry limit is exceeded. +- Make the bots automatically accept group chat contact requests ([#4377](https://github.com/deltachat/deltachat-core-rust/pull/4377)). +- Delete `smtp` rows when message sending is cancelled ([#4391](https://github.com/deltachat/deltachat-core-rust/pull/4391)). + +### Refactor + +- Iterate over `msg_ids` without .iter(). + +## [1.112.9] - 2023-05-12 + +### Fixes + +- Fetch at most 100 existing messages even if EXISTS was not received. +- Delete `smtp` rows when message sending is cancelled. + +### Changes + +- Improve SMTP logging. + +## [1.114.0] - 2023-04-24 + +### Changes +- JSON-RPC: Use long polling instead of server-sent notifications to retrieve events. + This better corresponds to JSON-RPC 2.0 server-client distinction + and is expected to simplify writing new bindings + because dispatching events can be done on higher level. +- JSON-RPC: TS: Client now has a mandatory argument whether you want to start listening for events. + +### Fixes +- JSON-RPC: do not print to stdout on failure to find an account. + + +## [1.113.0] - 2023-04-18 + +### Added +- New JSON-RPC API `can_send()`. +- New `dc_get_next_msgs()` and `dc_wait_next_msgs()` C APIs. + New `get_next_msgs()` and `wait_next_msgs()` JSON-RPC API. + These APIs can be used by bots to get all unprocessed messages + in the order of their arrival and wait for them without relying on events. +- New Python bindings API `Account.wait_next_incoming_message()`. +- New Python bindings APIs `Message.is_from_self()` and `Message.is_from_device()`. ### Changes - Increase MSRV to 1.65.0. #4236 - Remove upper limit on the attachment size. #4253 - Update rPGP to 0.10.1. #4236 -- Compress `mime_headers` column with HTML emails stored in database -- Strip BIDI characters in system messages, files, group names and contact names #3479 -- maybe_add_time_based_warnings(): Use release date instead of the provider DB update one -- Remove confusing log line "ignoring unsolicited response Recent(…)" #3934 -- Cleanly terminate deltachat-rpc-server. - Also terminate on ctrl-c. -- Refactorings #4317 -- Add JSON-RPC API `can_send()`. -- New `dc_get_next_msgs()` and `dc_wait_next_msgs()` C APIs. - New `get_next_msgs()` and `wait_next_msgs()` JSON-RPC API. - These APIs can be used by bots to get all unprocessed messages - in the order of their arrival and wait for them without relying on events. +- Compress HTML emails stored in the `mime_headers` column of the database. +- Strip BIDI characters in system messages, files, group names and contact names. #3479 +- Use release date instead of the provider database update date in `maybe_add_time_based_warnings()`. +- Gracefully terminate `deltachat-rpc-server` on Ctrl+C (`SIGINT`), `SIGTERM` and EOF. - Async Python API `get_fresh_messages_in_arrival_order()` is deprecated in favor of `get_next_msgs()` and `wait_next_msgs()`. -- New Python bindings API `Account.wait_next_incoming_message()`. -- New Python bindings APIs `Message.is_from_self()` and `Message.is_from_device()`. +- Remove metadata from avatars and JPEG images before sending. #4037 +- Recode PNG and other supported image formats to JPEG if they are > 500K in size. #4037 ### Fixes +- Don't let blocking be bypassed using groups. #4316 +- Show a warning if quota list is empty. #4261 +- Do not reset status on other devices when sending signed reaction messages. #3692 +- Update `accounts.toml` atomically. - Fix python bindings README documentation on installing the bindings from source. -- Show a warning if quota list is empty #4261 -- Update "accounts.toml" atomically -- Don't let blocking be bypassed using groups #4316 +- Remove confusing log line "ignoring unsolicited response Recent(…)". #3934 + +## [1.112.8] - 2023-04-20 + +### Changes +- Add `get_http_response` JSON-RPC API. +- Add C API to get HTTP responses. + +## [1.112.7] - 2023-04-17 + +### Fixes + +- Updated `async-imap` to v0.8.0 to fix erroneous EOF detection in long IMAP responses. ## [1.112.6] - 2023-04-04 @@ -2393,7 +2939,6 @@ For a full list of changes, please see our closed Pull Requests: https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed -[unreleased]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.5...HEAD [1.111.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.110.0...v1.111.0 [1.112.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.111.0...v1.112.0 [1.112.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.0...v1.112.1 @@ -2401,3 +2946,24 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed [1.112.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.2...v1.112.3 [1.112.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.3...v1.112.4 [1.112.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.4...v1.112.5 +[1.112.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.5...v1.112.6 +[1.112.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.6...v1.112.7 +[1.112.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.7...v1.112.8 +[1.112.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.8...v1.112.9 +[1.112.10]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.112.10 +[1.113.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.113.0 +[1.114.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.113.0...v1.114.0 +[1.115.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.114.0...v1.115.0 +[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.0...v1.116.0 +[1.117.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.116.0...v1.117.0 +[1.118.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.117.0...v1.118.0 +[1.119.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.118.0...v1.119.0 +[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1 +[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0 +[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0 +[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0 +[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0 +[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0 +[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1 +[1.125.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.1...v1.125.0 +[1.126.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.125.0...v1.126.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..0285b2697 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,115 @@ +# Contributing guidelines + +## Reporting bugs + +If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues). +If the bug you found is specific to +[Android](https://github.com/deltachat/deltachat-android/issues), +[iOS](https://github.com/deltachat/deltachat-ios/issues) or +[Desktop](https://github.com/deltachat/deltachat-desktop/issues), +report it to the corresponding repository. + +## Proposing features + +If you have a feature request, create a new topic on the [forum](https://support.delta.chat/). + +## Contributing code + +If you want to contribute a code, [open a Pull Request](https://github.com/deltachat/deltachat-core-rust/pulls). + +If you have write access to the repository, +push a branch named `/` +so it is clear who is responsible for the branch, +and open a PR proposing to merge the change. +Otherwise fork the repository and create a branch in your fork. + +You can find the list of good first issues +and a link to this guide +on the contributing page: + +### Coding conventions + +We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code. +Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy]. + +Commit messages follow the [Conventional Commits] notation. +We use [git-cliff] to generate the changelog from commit messages before the release. + +With **`git cliff --unreleased`**, you can check how the changelog entry for your commit will look. + +The following prefix types are used: +- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`. +- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled" +- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`" +- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`" +- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`" +- `test`: Test changes and improvements to the testing framework. +- `build`: Build system and tool configuration changes, e.g. "build(git-cliff): put "ci" commits into "CI" section of changelog" +- `ci`: CI configuration changes, e.g. "ci: limit artifact retention time for `libdeltachat.a` to 1 day" +- `docs`: Documentation changes, e.g. "docs: add contributing guidelines" +- `chore`: miscellaneous tasks, e.g. "chore: add `.DS_Store` to `.gitignore`" + +Release preparation commits are marked as "chore(release): prepare for vX.Y.Z". + +If you intend to squash merge the PR from the web interface, +make sure the PR title follows the conventional commits notation +as it will end up being a commit title. +Otherwise make sure each commit title follows the conventional commit notation. + +#### Breaking Changes + +Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`". + +Alternatively, breaking changes can go into the commit description, e.g.: + +``` +fix: Fix race condition and db corruption when a message was received during backup + +BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)` +``` + +#### Multiple Changes in one PR + +If you have multiple changes in one PR, create multiple conventional commits, and then do a rebase merge. Otherwise, you should usually do a squash merge. + +[Clippy]: https://doc.rust-lang.org/clippy/ +[Conventional Commits]: https://www.conventionalcommits.org/ +[git-cliff]: https://git-cliff.org/ + +### Errors + +Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors. +When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html), +capitalize it but do not add a full stop as the contexts will be separated by `:`. +For example: +``` +.with_context(|| format!("Unable to trash message {msg_id}")) +``` + +### Logging + +For logging, use `info!`, `warn!` and `error!` macros. +Log messages should be capitalized and have a full stop in the end. For example: +``` +info!(context, "Ignoring addition of {added_addr:?} to {chat_id}."); +``` + +Format anyhow errors with `{:#}` to print all the contexts like this: +``` +error!(context, "Failed to set selfavatar timestamp: {err:#}."); +``` + +### Reviewing + +Once a PR has an approval and passes CI, it can be merged. + +PRs from a branch created in the main repository, i.e. authored by those who have write access, are merged by their authors. +This is to ensure that PRs are merged as intended by the author, +e.g. as a squash merge, by rebasing from the web interface or manually from the command line. + +If you do not have access to the repository and created a PR from a fork, +ask the maintainers to merge the PR and say how it should be merged. + +## Other ways to contribute + +For other ways to contribute, refer to the [website](https://delta.chat/en/contribute). diff --git a/Cargo.lock b/Cargo.lock index 89b75936b..b8ae2c8af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - [[package]] name = "abao" version = "0.2.0" @@ -23,9 +17,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.19.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -38,9 +32,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aes" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher", @@ -49,30 +43,25 @@ dependencies = [ [[package]] name = "ahash" -version = "0.7.6" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "cd7d5a2cecb58716e47d67d5703a249964b14c7be1ec3cad3affc295b2d1c35d" dependencies = [ - "getrandom 0.2.8", + "cfg-if", "once_cell", "version_check", + "zerocopy", ] [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] -[[package]] -name = "aliasable" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" - [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -88,6 +77,18 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -113,25 +114,31 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.70" +name = "anstyle" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" dependencies = [ "backtrace", ] [[package]] name = "arrayref" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "ascii_utils" @@ -141,9 +148,9 @@ checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" [[package]] name = "asn1-rs" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf6690c370453db30743b373a60ba498fc0d6d83b11f4abfd87a84a075db5dd4" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -152,7 +159,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time 0.3.20", + "time 0.3.24", ] [[package]] @@ -180,9 +187,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", "event-listener", @@ -204,22 +211,21 @@ dependencies = [ [[package]] name = "async-imap" -version = "0.7.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8379e2f1cdeb79afd2006932d7e8f64993fc0f7386d0ebc37231c90b05968c25" +checksum = "936c1b580be4373b48c9c687e0c79285441664398354df28d0860087cac0c069" dependencies = [ "async-channel", - "async-native-tls 0.4.0", - "base64 0.21.0", - "byte-pool", + "base64 0.21.3", + "bytes", "chrono", "futures", "imap-proto", "log", "nom", "once_cell", - "ouroboros", "pin-utils", + "self_cell", "stop-token", "thiserror", "tokio", @@ -234,18 +240,6 @@ dependencies = [ "event-listener", ] -[[package]] -name = "async-native-tls" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d57d4cec3c647232e1094dc013546c0b33ce785d8aeb251e1f20dfaf8a9a13fe" -dependencies = [ - "native-tls", - "thiserror", - "tokio", - "url", -] - [[package]] name = "async-native-tls" version = "0.5.0" @@ -277,13 +271,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.29", ] [[package]] @@ -300,17 +294,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -319,13 +302,13 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.6.12" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f8ccfd9221ee7d1f3d4b33e1f8319b3a81ed8f61f2ea40b37b859794b4491" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", "axum-core", - "base64 0.21.0", + "base64 0.21.3", "bitflags 1.3.2", "bytes", "futures-util", @@ -354,9 +337,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2f958c80c248b34b9a877a643811be8dbca03ca5ba827f2b63baf3a81e5fc4e" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", "bytes", @@ -371,9 +354,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -416,9 +399,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.0" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" [[package]] name = "base64ct" @@ -449,22 +432,22 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.0.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" [[package]] name = "blake3" -version = "1.3.3" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" +checksum = "199c42ab6972d92c9f8995f086273d25c42fc0f7b2a1fcefba465c1352d25ba5" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -478,18 +461,18 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "block-padding" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ "generic-array", ] @@ -506,9 +489,9 @@ dependencies = [ [[package]] name = "brotli" -version = "3.3.4" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -517,9 +500,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.3.4" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -527,9 +510,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" +checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" dependencies = [ "memchr", "serde", @@ -547,19 +530,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" - -[[package]] -name = "byte-pool" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca" -dependencies = [ - "crossbeam-queue", - "stable_deref_trait", -] +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytemuck" @@ -591,18 +564,18 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.3" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6031a462f977dd38968b6f23378356512feeace69cef817e1a4475108093cec3" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" dependencies = [ "serde", ] [[package]] name = "cargo-platform" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" +checksum = "2cfa25e60aea747ec7e1124f238816749faa93759c6ff5b31f1ccdda137f4479" dependencies = [ "serde", ] @@ -637,9 +610,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.79" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "51f1226cd9da55587234753d1245dd5b132343ea240f26b6a9003d68706141ba" +dependencies = [ + "libc", +] [[package]] name = "cfb-mode" @@ -668,24 +644,24 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "95ed24df0632f708f5f6d8082675bef2596f7084dee3dd55f632290bf35bfe0f" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets 0.48.1", ] [[package]] name = "ciborium" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" dependencies = [ "ciborium-io", "ciborium-ll", @@ -694,15 +670,15 @@ dependencies = [ [[package]] name = "ciborium-io" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" [[package]] name = "ciborium-ll" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" dependencies = [ "ciborium-io", "half", @@ -710,9 +686,9 @@ dependencies = [ [[package]] name = "cipher" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", @@ -720,24 +696,28 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.23" +version = "4.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" dependencies = [ - "bitflags 1.3.2", + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +dependencies = [ + "anstyle", "clap_lex", - "indexmap", - "textwrap", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "clipboard-win" @@ -756,16 +736,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - [[package]] name = "color_quant" version = "1.1.0" @@ -774,33 +744,33 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "concurrent-queue" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" dependencies = [ "crossbeam-utils", ] [[package]] name = "const-oid" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747" [[package]] name = "const_format" -version = "0.2.30" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7309d9b4d3d2c0641e018d449232f2e28f1b22933c137f157d3dbc14228b8c0e" +checksum = "c990efc7a285731f9a4378d81aff2f0e85a2c8781a05ef0f8baa8dac54d0ff48" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.29" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f47bf7270cf70d370f8f98c1abb6d2d4cf60a6845d30e05bfb90c6568650" +checksum = "e026b6ce194a874cb9cf32cd5772d1ef9767cc8fcb5765948d74f37a9d8b2bf6" dependencies = [ "proc-macro2", "quote", @@ -809,9 +779,9 @@ dependencies = [ [[package]] name = "constant_time_eq" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" [[package]] name = "convert_case" @@ -837,15 +807,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] @@ -867,20 +837,20 @@ dependencies = [ [[package]] name = "criterion" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", - "atty", "cast", "ciborium", "clap", "criterion-plot", "futures", + "is-terminal", "itertools", - "lazy_static", "num-traits", + "once_cell", "oorandom", "plotters", "rayon", @@ -926,9 +896,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.14" +version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", "cfg-if", @@ -937,21 +907,11 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "crossbeam-queue" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] @@ -970,9 +930,9 @@ dependencies = [ [[package]] name = "crypto-bigint" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2538c4e68e52548bacb3e83ac549f903d44f011ac9d5abb5e132e67d0808f7" +checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -1005,61 +965,30 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0-rc.2" +version = "4.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d928d978dbec61a1167414f5ec534f24bea0d7a0d24dd9b6233d3d8223e585" +checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7" dependencies = [ "cfg-if", - "digest 0.10.6", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", "fiat-crypto", - "packed_simd_2", "platforms", + "rustc_version", "subtle", "zeroize", ] [[package]] -name = "cxx" -version = "1.0.91" +name = "curve25519-dalek-derive" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 1.0.109", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" +checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.29", ] [[package]] @@ -1074,12 +1003,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0808e1bd8671fb44a113a14e13497557533369847788fa2ae912b6ebfce9fa8" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ - "darling_core 0.14.3", - "darling_macro 0.14.3", + "darling_core 0.14.4", + "darling_macro 0.14.4", ] [[package]] @@ -1098,9 +1027,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "001d80444f28e193f30c2f293455da62dcf9a6b29918a4253152ae2b1de592cb" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" dependencies = [ "fnv", "ident_case", @@ -1123,20 +1052,20 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b36230598a2d5de7ec1c6f51f72d8a99a9208daff41de2084d06e3fd3ea56685" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ - "darling_core 0.14.3", + "darling_core 0.14.4", "quote", "syn 1.0.109", ] [[package]] name = "data-encoding" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "default-net" @@ -1152,22 +1081,22 @@ dependencies = [ "netlink-sys", "once_cell", "system-configuration", - "windows", + "windows 0.32.0", ] [[package]] name = "deltachat" -version = "1.112.6" +version = "1.126.1" dependencies = [ "ansi_term", "anyhow", "async-channel", "async-imap", - "async-native-tls 0.5.0", + "async-native-tls", "async-smtp", "async_zip", "backtrace", - "base64 0.21.0", + "base64 0.21.3", "brotli", "chrono", "criterion", @@ -1176,10 +1105,12 @@ dependencies = [ "encoded-words", "escaper", "fast-socks5", + "fd-lock", "format-flowed", "futures", "futures-lite", "hex", + "hickory-resolver", "humansize", "image", "iroh", @@ -1188,6 +1119,7 @@ dependencies = [ "libc", "log", "mailparse", + "mime", "num-derive", "num-traits", "num_cpus", @@ -1195,6 +1127,7 @@ dependencies = [ "parking_lot", "percent-encoding", "pgp", + "pretty_assertions", "pretty_env_logger", "proptest", "qrcodegen", @@ -1209,7 +1142,7 @@ dependencies = [ "serde", "serde_json", "sha-1", - "sha2 0.10.6", + "sha2 0.10.8", "smallvec", "strum", "strum_macros", @@ -1224,25 +1157,25 @@ dependencies = [ "tokio-tar", "tokio-util", "toml", - "trust-dns-resolver", "url", "uuid", ] [[package]] name = "deltachat-jsonrpc" -version = "1.112.6" +version = "1.126.1" dependencies = [ "anyhow", "async-channel", "axum", - "base64 0.21.0", + "base64 0.21.3", "deltachat", - "env_logger 0.10.0", + "env_logger", "futures", "log", "num-traits", "sanitize-filename", + "schemars", "serde", "serde_json", "tempfile", @@ -1254,7 +1187,7 @@ dependencies = [ [[package]] name = "deltachat-repl" -version = "1.112.6" +version = "1.126.1" dependencies = [ "ansi_term", "anyhow", @@ -1269,12 +1202,12 @@ dependencies = [ [[package]] name = "deltachat-rpc-server" -version = "1.112.6" +version = "1.126.1" dependencies = [ "anyhow", "deltachat", "deltachat-jsonrpc", - "env_logger 0.10.0", + "env_logger", "futures-lite", "log", "serde", @@ -1289,12 +1222,12 @@ name = "deltachat_derive" version = "2.0.0" dependencies = [ "quote", - "syn 2.0.13", + "syn 2.0.29", ] [[package]] name = "deltachat_ffi" -version = "1.112.6" +version = "1.126.1" dependencies = [ "anyhow", "deltachat", @@ -1307,6 +1240,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", + "yerpc", ] [[package]] @@ -1323,9 +1257,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.1" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc906908ea6458456e5eaa160a9c08543ec3d1e6f71e2235cedd660cb65f9df0" +checksum = "0c7ed52955ce76b1554f509074bb357d3fb8ac9b51288a65a3fd480d1dfba946" dependencies = [ "const-oid", "pem-rfc7468 0.7.0", @@ -1334,9 +1268,9 @@ dependencies = [ [[package]] name = "der-parser" -version = "8.1.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d4bc9b0db0a0df9ae64634ac5bdefb7afcb534e182275ca0beadbe486701c1" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" dependencies = [ "asn1-rs", "displaydoc", @@ -1358,6 +1292,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "deranged" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8810e7e2cf385b1e9b50d68264908ec367ba642c96d02edfe61c39e88e2a3c01" + [[package]] name = "derive_builder" version = "0.12.0" @@ -1373,7 +1313,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" dependencies = [ - "darling 0.14.3", + "darling 0.14.4", "proc-macro2", "quote", "syn 1.0.109", @@ -1411,6 +1351,12 @@ dependencies = [ "cipher", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.9.0" @@ -1422,11 +1368,11 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.3", + "block-buffer 0.10.4", "const-oid", "crypto-common", "subtle", @@ -1434,9 +1380,9 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dece029acd3353e3a58ac2e3eb3c8d6c35827a892edc6cc4138ef9c33df46ecd" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys", ] @@ -1453,13 +1399,14 @@ dependencies = [ [[package]] name = "dirs-sys" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", + "option-ext", "redox_users", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -1475,13 +1422,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.29", ] [[package]] @@ -1507,6 +1454,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dyn-clone" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" + [[package]] name = "ecdsa" version = "0.14.8" @@ -1521,14 +1474,16 @@ dependencies = [ [[package]] name = "ecdsa" -version = "0.16.2" +version = "0.16.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644d3b8674a5fc5b929ae435bca85c2323d85ccb013a5509c2ac9ee11a6284ba" +checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" dependencies = [ - "der 0.7.1", - "elliptic-curve 0.13.2", + "der 0.7.7", + "digest 0.10.7", + "elliptic-curve 0.13.5", "rfc6979 0.4.0", - "signature 2.0.0", + "signature 2.1.0", + "spki 0.7.2", ] [[package]] @@ -1543,12 +1498,12 @@ dependencies = [ [[package]] name = "ed25519" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be522bee13fa6d8059f4903a4084aa3bd50725e18150202f0238deb615cd6371" +checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963" dependencies = [ - "pkcs8 0.10.1", - "signature 2.0.0", + "pkcs8 0.10.2", + "signature 2.1.0", ] [[package]] @@ -1568,22 +1523,22 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.0.0-rc.2" +version = "2.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798f704d128510932661a3489b08e3f4c934a01d61c5def59ae7b8e48f19665a" +checksum = "faa8e9049d5d72bfc12acbc05914731b5322f79b5e2f195e9f2d705fca22ab4c" dependencies = [ - "curve25519-dalek 4.0.0-rc.2", - "ed25519 2.2.0", + "curve25519-dalek 4.0.0-rc.3", + "ed25519 2.2.1", "serde", - "sha2 0.10.6", + "sha2 0.10.8", "zeroize", ] [[package]] name = "educe" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0188e3c3ba8df5753894d54461f0e39bc91741dc5b22e1c46999ec2c71f4e4" +checksum = "079044df30bb07de7d846d41a184c4b00e66ebdac93ee459253474f3a47e50ae" dependencies = [ "enum-ordinalize", "proc-macro2", @@ -1593,9 +1548,9 @@ dependencies = [ [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" @@ -1606,7 +1561,7 @@ dependencies = [ "base16ct 0.1.1", "crypto-bigint 0.4.9", "der 0.6.1", - "digest 0.10.6", + "digest 0.10.7", "ff 0.12.1", "generic-array", "group 0.12.1", @@ -1618,21 +1573,21 @@ dependencies = [ [[package]] name = "elliptic-curve" -version = "0.13.2" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea5a92946e8614bb585254898bb7dd1ddad241ace60c52149e3765e34cc039d" +checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" dependencies = [ "base16ct 0.2.0", - "crypto-bigint 0.5.1", - "digest 0.10.6", + "crypto-bigint 0.5.2", + "digest 0.10.7", "ff 0.13.0", "generic-array", "group 0.13.0", "hkdf", "pem-rfc7468 0.7.0", - "pkcs8 0.10.1", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1 0.7.1", + "sec1 0.7.3", "subtle", "zeroize", ] @@ -1640,7 +1595,7 @@ dependencies = [ [[package]] name = "email" version = "0.0.21" -source = "git+https://github.com/deltachat/rust-email?branch=master#25702df99254d059483b41417cd80696a258df8e" +source = "git+https://github.com/deltachat/rust-email?branch=master#37778c89d5eb5a94b7983f3f37ff67769bde3cf9" dependencies = [ "base64 0.11.0", "chrono", @@ -1753,41 +1708,27 @@ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" [[package]] name = "enum-as-inner" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" dependencies = [ "heck", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.29", ] [[package]] name = "enum-ordinalize" -version = "3.1.12" +version = "3.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bb1df8b45ecb7ffa78dca1c17a438fb193eb083db0b1b494d2a61bcb5096a" +checksum = "e4f76552f53cefc9a7f64987c3701b99d982f7690606fd67de1d09712fbf52f1" dependencies = [ "num-bigint", "num-traits", "proc-macro2", "quote", - "rustc_version", - "syn 1.0.109", -] - -[[package]] -name = "env_logger" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" -dependencies = [ - "atty", - "humantime 1.3.0", - "log", - "regex", - "termcolor", + "syn 2.0.29", ] [[package]] @@ -1796,7 +1737,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" dependencies = [ - "humantime 2.1.0", + "humantime", "is-terminal", "log", "regex", @@ -1804,14 +1745,20 @@ dependencies = [ ] [[package]] -name = "errno" -version = "0.3.0" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -1893,14 +1840,29 @@ dependencies = [ ] [[package]] -name = "fd-lock" -version = "3.0.11" +name = "fastrand" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9799aefb4a2e4a01cc47610b1dd47c18ab13d991f27bbcaed9296f5a53d5cbad" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "fd-lock" +version = "3.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" dependencies = [ "cfg-if", "rustix", - "windows-sys 0.45.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", ] [[package]] @@ -1925,27 +1887,27 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ace6ec7cc19c8ed33a32eaa9ea692d7faea05006b5356b9e2b668ec4bc3955" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" [[package]] name = "filetime" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" dependencies = [ "cfg-if", "libc", "redox_syscall 0.2.16", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", "miniz_oxide", @@ -1987,9 +1949,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -2048,11 +2010,11 @@ checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-lite" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -2069,7 +2031,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.29", ] [[package]] @@ -2104,9 +2066,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -2126,9 +2088,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "js-sys", @@ -2149,9 +2111,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "group" @@ -2177,9 +2139,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.17" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" dependencies = [ "bytes", "fnv", @@ -2187,7 +2149,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -2205,17 +2167,24 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" dependencies = [ "ahash", + "allocator-api2", ] [[package]] name = "hashlink" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" dependencies = [ - "hashbrown", + "hashbrown 0.14.2", ] [[package]] @@ -2226,27 +2195,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "hex" @@ -2254,6 +2205,51 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-proto" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091a6fbccf4860009355e3efc52ff4acf37a63489aad7435372d44ceeb6fbbcf" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.8.5", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b8f021164e6a984c9030023544c57789c51760065cd510572fedcfb04164e8" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot", + "rand 0.8.5", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.3" @@ -2269,7 +2265,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", ] [[package]] @@ -2319,9 +2324,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "human-panic" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6557b29bbdc9d6c7a5cdbe2962e78eaf48115e8d55b0b62282956981c1f605" +checksum = "38a841f87949b0dd751864e769a870be79dc34abcee1cf31d737a61d498b22b6" dependencies = [ "backtrace", "os_info", @@ -2337,16 +2342,7 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" dependencies = [ - "libm 0.2.6", -] - -[[package]] -name = "humantime" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" -dependencies = [ - "quick-error 1.2.3", + "libm", ] [[package]] @@ -2357,9 +2353,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.24" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -2372,7 +2368,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -2394,26 +2390,25 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows 0.48.0", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] @@ -2433,20 +2428,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.2.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "idna" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -2454,9 +2438,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" dependencies = [ "bytemuck", "byteorder", @@ -2479,12 +2463,22 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.2", ] [[package]] @@ -2505,33 +2499,23 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "io-lifetimes" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" -dependencies = [ - "libc", - "windows-sys 0.45.0", -] - [[package]] name = "ipconfig" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.3", "widestring", - "winapi", + "windows-sys 0.48.0", "winreg", ] [[package]] name = "ipnet" -version = "2.7.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "iroh" @@ -2541,7 +2525,7 @@ checksum = "e4fb9858c8cd3dd924a5da5bc511363845a9bcfdfac066bb2ef8454eb6111546" dependencies = [ "abao", "anyhow", - "base64 0.21.0", + "base64 0.21.3", "blake3", "bytes", "default-net", @@ -2579,14 +2563,13 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.6" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256017f749ab3117e93acb91063009e1f1bb56d03965b14c2c8df4eb02c524d8" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", + "hermit-abi", "rustix", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -2600,9 +2583,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jpeg-decoder" @@ -2612,9 +2595,9 @@ checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -2630,9 +2613,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" dependencies = [ "cpufeatures", ] @@ -2672,21 +2655,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.140" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libm" -version = "0.1.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" - -[[package]] -name = "libm" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libsqlite3-sys" @@ -2700,15 +2677,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2717,15 +2685,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.1" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -2733,12 +2701,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "lru-cache" @@ -2772,20 +2737,14 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "matchit" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" +checksum = "67827e6ea8ee8a7c4a72227ef4fc08957040acffdb5f122733b24fa12daff41b" [[package]] name = "md-5" @@ -2793,7 +2752,7 @@ version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -2804,24 +2763,24 @@ checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memoffset" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" @@ -2831,23 +2790,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", + "simd-adler32", ] [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -2862,7 +2821,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.10", ] [[package]] @@ -2965,9 +2924,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" dependencies = [ "winapi", ] @@ -2995,13 +2954,13 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ "byteorder", "lazy_static", - "libm 0.2.6", + "libm", "num-integer", "num-iter", "num-traits", @@ -3013,13 +2972,13 @@ dependencies = [ [[package]] name = "num-derive" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.29", ] [[package]] @@ -3056,29 +3015,29 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", - "libm 0.2.6", + "libm", ] [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi", "libc", ] [[package]] name = "object" -version = "0.30.3" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" dependencies = [ "memchr", ] @@ -3094,9 +3053,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "oorandom" @@ -3112,11 +3071,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.48" +version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518915b97df115dd36109bfa429a48b8f737bd05508cf9588977b599648926d2" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.3.3", "cfg-if", "foreign-types", "libc", @@ -3127,13 +3086,13 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.29", ] [[package]] @@ -3144,20 +3103,19 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.25.1+1.1.1t" +version = "300.1.5+3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ef9a9cc6ea7d9d5e7c4a913dc4b48d0e359eddf01af1dfec96ba7064b4aba10" +checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.83" +version = "0.9.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666416d899cf077260dac8698d60a60b435a46d57e82acb1be3d0dad87284e5b" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" dependencies = [ - "autocfg", "cc", "libc", "openssl-src", @@ -3166,45 +3124,22 @@ dependencies = [ ] [[package]] -name = "os_info" -version = "3.6.0" +name = "option-ext" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c424bc68d15e0778838ac013b5b3449544d8133633d8016319e7e05a820b8c0" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_info" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" dependencies = [ "log", "serde", "winapi", ] -[[package]] -name = "os_str_bytes" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" - -[[package]] -name = "ouroboros" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1358bd1558bd2a083fed428ffeda486fbfb323e698cdda7794259d592ca72db" -dependencies = [ - "aliasable", - "ouroboros_macro", -] - -[[package]] -name = "ouroboros_macro" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7d21ccd03305a674437ee1248f3ab5d4b1db095cf1caf49f1713ddf61956b7" -dependencies = [ - "Inflector", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "overload" version = "0.1.1" @@ -3219,19 +3154,19 @@ checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ "ecdsa 0.14.8", "elliptic-curve 0.12.3", - "sha2 0.10.6", + "sha2 0.10.8", ] [[package]] name = "p256" -version = "0.13.0" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7270da3e5caa82afd3deb054cc237905853813aea3859544bc082c3fe55b8d47" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa 0.16.2", - "elliptic-curve 0.13.2", + "ecdsa 0.16.8", + "elliptic-curve 0.13.5", "primeorder", - "sha2 0.10.6", + "sha2 0.10.8", ] [[package]] @@ -3242,7 +3177,7 @@ checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" dependencies = [ "ecdsa 0.14.8", "elliptic-curve 0.12.3", - "sha2 0.10.6", + "sha2 0.10.8", ] [[package]] @@ -3251,27 +3186,17 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" dependencies = [ - "ecdsa 0.16.2", - "elliptic-curve 0.13.2", + "ecdsa 0.16.8", + "elliptic-curve 0.13.5", "primeorder", - "sha2 0.10.6", -] - -[[package]] -name = "packed_simd_2" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" -dependencies = [ - "cfg-if", - "libm 0.1.4", + "sha2 0.10.8", ] [[package]] name = "parking" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" [[package]] name = "parking_lot" @@ -3285,22 +3210,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "smallvec", - "windows-sys 0.45.0", + "windows-targets 0.48.1", ] [[package]] name = "paste" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pem" @@ -3331,18 +3256,18 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pgp" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a79d6411154d1a9908e7a2c4bac60a5742f6125823c2c30780c7039aef02f0" +checksum = "27e1f8e085bfa9b85763fe3ddaacbe90a09cd847b3833129153a6cb063bbe132" dependencies = [ "aes", - "base64 0.21.0", + "base64 0.21.3", "bitfield", "block-padding", "blowfish", @@ -3355,11 +3280,12 @@ dependencies = [ "chrono", "cipher", "crc24", + "curve25519-dalek 4.0.0-rc.3", "derive_builder", "des", - "digest 0.10.6", - "ed25519-dalek 2.0.0-rc.2", - "elliptic-curve 0.13.2", + "digest 0.10.7", + "ed25519-dalek 2.0.0-rc.3", + "elliptic-curve 0.13.5", "flate2", "generic-array", "hex", @@ -3370,15 +3296,15 @@ dependencies = [ "num-bigint-dig", "num-derive", "num-traits", - "p256 0.13.0", + "p256 0.13.2", "p384 0.13.0", "rand 0.8.5", "ripemd", - "rsa 0.9.0-pre.0", + "rsa 0.9.2", "sha1", - "sha2 0.10.6", + "sha2 0.10.8", "sha3", - "signature 2.0.0", + "signature 2.1.0", "smallvec", "thiserror", "twofish", @@ -3388,29 +3314,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.12" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.12" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.29", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -3432,14 +3358,13 @@ dependencies = [ [[package]] name = "pkcs1" -version = "0.7.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "178ba28ece1961eafdff1991bd1744c29564cbab5d803f3ccb4a4895a6c550a7" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der 0.7.1", - "pkcs8 0.10.1", - "spki 0.7.0", - "zeroize", + "der 0.7.7", + "pkcs8 0.10.2", + "spki 0.7.2", ] [[package]] @@ -3454,19 +3379,19 @@ dependencies = [ [[package]] name = "pkcs8" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d2820d87d2b008616e5c27212dd9e0e694fb4c6b522de06094106813328cb49" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der 0.7.1", - "spki 0.7.0", + "der 0.7.7", + "spki 0.7.2", ] [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "platforms" @@ -3476,9 +3401,9 @@ checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" [[package]] name = "plotters" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" dependencies = [ "num-traits", "plotters-backend", @@ -3489,42 +3414,43 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" [[package]] name = "plotters-svg" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" dependencies = [ "plotters-backend", ] [[package]] name = "png" -version = "0.17.7" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" dependencies = [ "bitflags 1.3.2", "crc32fast", + "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "portable-atomic" -version = "1.0.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39c00c8683a03bd4fe7db7dd64ab4abee6b42166bc81231da983486ce96be51a" +checksum = "f32154ba0af3a075eefa1eda8bb414ee928f62303a54ea85b8d6638ff1a6ee9e" [[package]] name = "postcard" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa512cd0d087cc9f99ad30a1bf64795b67871edbead083ffc3a4dfafa59aa00" +checksum = "c9ee729232311d3cd113749948b689627618133b1c5012b77342c1950b25eaeb" dependencies = [ "cobs", "const_format", @@ -3550,22 +3476,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] -name = "pretty_env_logger" -version = "0.4.0" +name = "pretty_assertions" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" dependencies = [ - "env_logger 0.7.1", + "diff", + "yansi", +] + +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", "log", ] [[package]] name = "primeorder" -version = "0.13.0" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7613fdcc0831c10060fa69833ea8fa2caa94b6456f51e25356a885b530a2e3d0" +checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3" dependencies = [ - "elliptic-curve 0.13.2", + "elliptic-curve 0.13.5", ] [[package]] @@ -3594,28 +3530,26 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.55" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0dd4be24fcdcfeaa12a432d588dc59bbad6cad3510c67e74a2b6b2fc950564" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.1.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f1b898011ce9595050a68e60f90bad083ff2987a695a42357134c8381fba70" +checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e" dependencies = [ - "bitflags 1.3.2", - "byteorder", + "bitflags 2.3.3", "lazy_static", "num-traits", - "quick-error 2.0.1", "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax", + "regex-syntax 0.7.5", "unarray", ] @@ -3649,17 +3583,11 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quick-xml" -version = "0.28.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c1a97b1bc42b1d550bfb48d4262153fe400a12bab1511821736f7eac76d7e2" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" dependencies = [ "memchr", ] @@ -3707,25 +3635,25 @@ source = "git+https://github.com/quinn-rs/quinn?branch=main#11b34a7b2652010cdbbd dependencies = [ "libc", "quinn-proto", - "socket2", + "socket2 0.4.9", "tracing", "windows-sys 0.45.0", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "quoted_printable" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a24039f627d8285853cc90dcddf8c1ebfaa91f834566948872b225b9a28ed1b6" +checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49" [[package]] name = "radix_trie" @@ -3796,7 +3724,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.10", ] [[package]] @@ -3823,9 +3751,9 @@ version = "1.0.0" [[package]] name = "rayon" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" dependencies = [ "either", "rayon-core", @@ -3833,9 +3761,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.10.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -3851,7 +3779,7 @@ checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" dependencies = [ "pem", "ring", - "time 0.3.20", + "time 0.3.24", "yasna", ] @@ -3879,20 +3807,21 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.10", "redox_syscall 0.2.16", "thiserror", ] [[package]] name = "regex" -version = "1.7.3" +version = "1.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-automata 0.3.9", + "regex-syntax 0.7.5", ] [[package]] @@ -3901,7 +3830,18 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ - "regex-syntax", + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", ] [[package]] @@ -3911,12 +3851,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] -name = "reqwest" -version = "0.11.16" +name = "regex-syntax" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "reqwest" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ - "base64 0.21.0", + "base64 0.21.3", "bytes", "encoding_rs", "futures-core", @@ -3954,7 +3900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" dependencies = [ "hostname", - "quick-error 1.2.3", + "quick-error", ] [[package]] @@ -3999,7 +3945,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -4009,7 +3955,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c" dependencies = [ "byteorder", - "digest 0.10.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-iter", @@ -4025,20 +3971,22 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.0-pre.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7bc1d34159d63536b4d89944e9ab5bb952f45db6fa0b8b03c2f8c09fb5b7171" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" dependencies = [ "byteorder", - "digest 0.10.6", + "const-oid", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-iter", "num-traits", - "pkcs1 0.7.1", - "pkcs8 0.10.1", + "pkcs1 0.7.5", + "pkcs8 0.10.2", "rand_core 0.6.4", - "signature 2.0.0", + "signature 2.1.0", + "spki 0.7.2", "subtle", "zeroize", ] @@ -4049,7 +3997,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 2.0.2", + "bitflags 2.3.3", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -4065,9 +4013,9 @@ checksum = "efe2374f2385cdd8755a446f80b2a646de603c9d8539ca38734879b5c71e378b" [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc-hash" @@ -4095,16 +4043,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.6" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d097081ed288dfe45699b72f5b5d648e5f15d64d900c7080273baa20c16a6849" +checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.3.3", "errno", - "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -4120,9 +4067,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", "rustls-pemfile", @@ -4132,30 +4079,30 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.0", + "base64 0.21.3", ] [[package]] name = "rustversion" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rustyline" -version = "11.0.0" +version = "12.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfc8644681285d1fb67a467fb3021bfea306b99b4146b166a1fe3ada965eece" +checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.3.3", "cfg-if", "clipboard-win", - "dirs-next", "fd-lock", + "home", "libc", "log", "memchr", @@ -4170,9 +4117,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.12" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "safemem" @@ -4191,9 +4138,9 @@ dependencies = [ [[package]] name = "sanitize-filename" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c" +checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603" dependencies = [ "lazy_static", "regex", @@ -4201,24 +4148,42 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys 0.42.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "schemars" +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", ] [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" @@ -4246,23 +4211,23 @@ dependencies = [ [[package]] name = "sec1" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48518a2b5775ba8ca5b46596aae011caa431e6ce7e4a67ead66d92f08884220e" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct 0.2.0", - "der 0.7.1", + "der 0.7.7", "generic-array", - "pkcs8 0.10.1", + "pkcs8 0.10.2", "subtle", "zeroize", ] [[package]] name = "security-framework" -version = "2.8.2" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -4273,28 +4238,34 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.8.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", ] [[package]] -name = "semver" -version = "1.0.16" +name = "self_cell" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" +checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.159" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] @@ -4310,29 +4281,40 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.9" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.159" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.29", +] + +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] name = "serde_json" -version = "1.0.95" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", @@ -4341,18 +4323,19 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.9" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b04f22b563c91331a10074bda3dd5492e3cc39d56bd557e91c0af42b6c7341" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" dependencies = [ + "itoa", "serde", ] [[package]] name = "serde_spanned" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ "serde", ] @@ -4377,7 +4360,7 @@ checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -4388,7 +4371,7 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -4406,22 +4389,22 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] name = "sha3" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "keccak", ] @@ -4449,20 +4432,26 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "rand_core 0.6.4", ] [[package]] name = "signature" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.8" @@ -4474,9 +4463,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "smawk" @@ -4494,6 +4483,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.5.2" @@ -4521,12 +4520,12 @@ dependencies = [ [[package]] name = "spki" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0445c905640145c7ea8c1993555957f65e7c46d0535b91ba501bc9bfc85522f" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" dependencies = [ "base64ct", - "der 0.7.1", + "der 0.7.7", ] [[package]] @@ -4537,7 +4536,7 @@ checksum = "19cfdc32e0199062113edf41f344fbf784b8205a94600233c84eb838f45191e1" dependencies = [ "base64ct", "pem-rfc7468 0.6.0", - "sha2 0.10.6", + "sha2 0.10.8", ] [[package]] @@ -4552,18 +4551,12 @@ dependencies = [ "rand_core 0.6.4", "rsa 0.7.2", "sec1 0.3.0", - "sha2 0.10.6", + "sha2 0.10.8", "signature 1.6.4", "ssh-encoding", "zeroize", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "static_assertions" version = "1.1.0" @@ -4596,28 +4589,28 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" -version = "0.24.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" [[package]] name = "strum_macros" -version = "0.24.3" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", - "syn 1.0.109", + "syn 2.0.29", ] [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -4632,9 +4625,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.13" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -4675,9 +4668,9 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75182f12f490e953596550b65ee31bda7c8e043d9386174b353bda50838c3fd" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -4696,21 +4689,21 @@ dependencies = [ [[package]] name = "tagger" -version = "4.3.4" +version = "4.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aaa6f5d645d1dae4cd0286e9f8bf15b75a31656348e5e106eb1a940abd34b63" +checksum = "094c9f64d6de9a8506b1e49b63a29333b37ed9e821ee04be694d431b3264c3c5" [[package]] name = "tempfile" -version = "3.5.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", - "fastrand", + "fastrand 2.0.0", "redox_syscall 0.3.5", "rustix", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -4724,9 +4717,9 @@ dependencies = [ [[package]] name = "testdir" -version = "0.7.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fc921e7c4ad1aedb3484811514f3e5cd187886e0bbf1302c175f7578ef552" +checksum = "48b7965698cfb3d1ac1e6e54b4b45f5caa9e89bda223c8cf723d9cf53d7cefa7" dependencies = [ "anyhow", "backtrace", @@ -4749,22 +4742,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.29", ] [[package]] @@ -4790,10 +4783,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.20" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "b79eabcd964882a646b3584543ccabeae7869e9ac32a46f6f22b7a5bd405308b" dependencies = [ + "deranged", "itoa", "serde", "time-core", @@ -4802,15 +4796,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" dependencies = [ "time-core", ] @@ -4842,11 +4836,11 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.27.0" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -4854,9 +4848,9 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.3", "tokio-macros", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -4871,13 +4865,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.29", ] [[package]] @@ -4907,9 +4901,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", @@ -4918,14 +4912,14 @@ dependencies = [ [[package]] name = "tokio-tar" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50188549787c32c1c3d9c8c71ad7e003ccf2f102489c5a96e385c84760477f4" +checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75" dependencies = [ "filetime", "futures-core", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "tokio", "tokio-stream", "xattr", @@ -4933,9 +4927,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.18.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", @@ -4945,9 +4939,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes", "futures-core", @@ -4959,9 +4953,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.3" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" dependencies = [ "serde", "serde_spanned", @@ -4971,20 +4965,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.8" +version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" dependencies = [ - "indexmap", + "indexmap 2.0.0", "serde", "serde_spanned", "toml_datetime", @@ -5034,20 +5028,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.29", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", "valuable", @@ -5076,9 +5070,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ "matchers", "nu-ansi-term", @@ -5092,51 +5086,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "trust-dns-proto" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna 0.2.3", - "ipnet", - "lazy_static", - "rand 0.8.5", - "smallvec", - "thiserror", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "trust-dns-resolver" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" -dependencies = [ - "cfg-if", - "futures-util", - "ipconfig", - "lazy_static", - "lru-cache", - "parking_lot", - "resolv-conf", - "smallvec", - "thiserror", - "tokio", - "tracing", - "trust-dns-proto", -] - [[package]] name = "try-lock" version = "0.2.4" @@ -5145,13 +5094,13 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.18.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ - "base64 0.13.1", "byteorder", "bytes", + "data-encoding", "http", "httparse", "log", @@ -5179,9 +5128,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "typescript-type-def" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e6b74ffbd5684d318252bb7182051df8c4ecc098b542f63fddf792e7f42aa02" +checksum = "356e00027bd9ef773605a353070dc87684b25561a59087ea3ee3dd5fe8854e83" dependencies = [ "serde_json", "typescript-type-def-derive", @@ -5189,9 +5138,9 @@ dependencies = [ [[package]] name = "typescript-type-def-derive" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10a4f5dd87c279f90beef31edb7055bfd1ceb66e73148de107a5c9005e9f864" +checksum = "c4e696c28431595138cc53892104528152cbcf26653ae0aa655e4eaede5b9f69" dependencies = [ "darling 0.13.4", "ident_case", @@ -5209,25 +5158,21 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-bidi" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.6" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "unicode-linebreak" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" -dependencies = [ - "hashbrown", - "regex", -] +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" @@ -5264,12 +5209,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.3.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", - "idna 0.3.0", + "idna", "percent-encoding", ] @@ -5281,17 +5226,17 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8parse" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.10", "serde", ] @@ -5331,11 +5276,10 @@ dependencies = [ [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -5359,9 +5303,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -5369,24 +5313,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.29", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if", "js-sys", @@ -5396,9 +5340,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5406,28 +5350,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -5435,9 +5379,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.0" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f" dependencies = [ "ring", "untrusted", @@ -5451,9 +5395,9 @@ checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" [[package]] name = "whoami" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45dbc71f0cdca27dc261a9bd37ddec174e4a0af2b900b890f378460f745426e3" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" dependencies = [ "wasm-bindgen", "web-sys", @@ -5461,9 +5405,9 @@ dependencies = [ [[package]] name = "widestring" -version = "0.5.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" [[package]] name = "winapi" @@ -5510,18 +5454,12 @@ dependencies = [ ] [[package]] -name = "windows-sys" -version = "0.42.0" +name = "windows" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.1", + "windows-targets 0.48.1", ] [[package]] @@ -5530,7 +5468,16 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.1", ] [[package]] @@ -5539,20 +5486,41 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.1", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" @@ -5562,9 +5530,15 @@ checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" @@ -5574,9 +5548,15 @@ checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" [[package]] name = "windows_i686_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" @@ -5586,9 +5566,15 @@ checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" [[package]] name = "windows_i686_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" @@ -5598,15 +5584,27 @@ checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" @@ -5616,36 +5614,44 @@ checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +checksum = "f46aab759304e4d7b2075a9aecba26228bb073ee8c50db796b2c72c676b5d807" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", ] [[package]] name = "x25519-dalek" -version = "2.0.0-pre.1" +version = "2.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" +checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a" dependencies = [ - "curve25519-dalek 3.2.0", + "curve25519-dalek 4.0.0-rc.3", "rand_core 0.6.4", + "serde", "zeroize", ] @@ -5664,32 +5670,38 @@ dependencies = [ "oid-registry", "rusticata-macros", "thiserror", - "time 0.3.20", + "time 0.3.24", ] [[package]] name = "xattr" -version = "0.2.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" dependencies = [ "libc", ] [[package]] -name = "yasna" +name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aed2e7a52e3744ab4d0c05c20aa065258e84c49fd4226f5191b2ed29712710b4" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ - "time 0.3.20", + "time 0.3.24", ] [[package]] name = "yerpc" -version = "0.4.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a0257f42e6bdc187f37074723b6094da3502cee21ae517b3c54d2c37d506e7" +checksum = "75b5547af776328f66a5476ea3b7c0789e6fed164eb32d1a2122cfb39ffa505d" dependencies = [ "anyhow", "async-channel", @@ -5699,6 +5711,7 @@ dependencies = [ "futures", "futures-util", "log", + "schemars", "serde", "serde_json", "tokio", @@ -5709,34 +5722,53 @@ dependencies = [ [[package]] name = "yerpc_derive" -version = "0.4.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd53ff9053698697b92c2535bf7ecb983fd5d546d690b7c725e5070d6d9a620" +checksum = "f321bb5f728fb066af06c5a994e4375f1f8b054ee6d650766f0bd68dfa4faefe" dependencies = [ "convert_case 0.5.0", - "darling 0.14.3", + "darling 0.14.4", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] -name = "zeroize" -version = "1.5.7" +name = "zerocopy" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "7a7af71d8643341260a65f89fa60c0eeaa907f34544d8f6d9b0df72f069b5e74" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9731702e2f0617ad526794ae28fbc6f6ca8849b5ba729666c2a5bc4b6ddee2cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.3.3" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", - "synstructure", + "syn 2.0.29", ] diff --git a/Cargo.toml b/Cargo.toml index 281aeeb41..e71720265 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "deltachat" -version = "1.112.6" +version = "1.126.1" edition = "2021" license = "MPL-2.0" -rust-version = "1.65" +rust-version = "1.67" [profile.dev] debug = 0 @@ -23,6 +23,8 @@ opt-level = "z" lto = true panic = 'abort' opt-level = "z" +codegen-units = 1 +strip = true [patch.crates-io] quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" } @@ -35,74 +37,77 @@ ratelimit = { path = "./deltachat-ratelimit" } anyhow = "1" async-channel = "1.8.0" -async-imap = { version = "0.7.0", default-features = false, features = ["runtime-tokio"] } +async-imap = { version = "0.9.1", default-features = false, features = ["runtime-tokio"] } async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] } async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] } async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] } backtrace = "0.3" base64 = "0.21" -brotli = "3.3" +brotli = { version = "3.4", default-features=false, features = ["std"] } chrono = { version = "0.4", default-features=false, features = ["clock", "std"] } email = { git = "https://github.com/deltachat/rust-email", branch = "master" } encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" } escaper = "0.1" fast-socks5 = "0.8" +fd-lock = "3.0.11" futures = "0.3" -futures-lite = "1.12.0" +futures-lite = "1.13.0" hex = "0.4.0" +hickory-resolver = "0.24" humansize = "2" -image = { version = "0.24.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] } +image = { version = "0.24.7", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] } iroh = { version = "0.4.1", default-features = false } kamadak-exif = "0.5" lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" } libc = "0.2" mailparse = "0.14" -num_cpus = "1.15" -num-derive = "0.3" +mime = "0.3.17" +num_cpus = "1.16" +num-derive = "0.4" num-traits = "0.2" -once_cell = "1.17.0" -percent-encoding = "2.2" +once_cell = "1.18.0" +percent-encoding = "2.3" parking_lot = "0.12" pgp = { version = "0.10", default-features = false } -pretty_env_logger = { version = "0.4", optional = true } +pretty_env_logger = { version = "0.5", optional = true } qrcodegen = "1.7.0" -quick-xml = "0.28" +quick-xml = "0.30" rand = "0.8" -regex = "1.7" -reqwest = { version = "0.11.16", features = ["json"] } +regex = "1.9" +reqwest = { version = "0.11.20", features = ["json"] } rusqlite = { version = "0.29", features = ["sqlcipher"] } rust-hsluv = "0.1" -sanitize-filename = "0.4" +sanitize-filename = "0.5" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } sha-1 = "0.10" sha2 = "0.10" smallvec = "1" -strum = "0.24" -strum_macros = "0.24" +strum = "0.25" +strum_macros = "0.25" tagger = "4.3.4" textwrap = "0.16.0" thiserror = "1" tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] } tokio-io-timeout = "1.2.0" -tokio-stream = { version = "0.1.11", features = ["fs"] } +tokio-stream = { version = "0.1.14", features = ["fs"] } tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar -tokio-util = "0.7.7" +tokio-util = "0.7.9" toml = "0.7" -trust-dns-resolver = "0.22" url = "2" uuid = { version = "1", features = ["serde", "v4"] } [dev-dependencies] ansi_term = "0.12.0" -criterion = { version = "0.4.0", features = ["async_tokio"] } -futures-lite = "1.12" +criterion = { version = "0.5.1", features = ["async_tokio"] } +futures-lite = "1.13" log = "0.4" -pretty_env_logger = "0.4" +pretty_env_logger = "0.5" proptest = { version = "1", default-features = false, features = ["std"] } tempfile = "3" -testdir = "0.7.3" +testdir = "0.8.0" tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] } +pretty_assertions = "1.3.0" [workspace] members = [ @@ -115,11 +120,6 @@ members = [ "format-flowed", ] -[[example]] -name = "simple" -path = "examples/simple.rs" - - [[bench]] name = "create_account" harness = false diff --git a/LICENSE b/LICENSE index c7a27cda5..7d0aa6a5c 100644 --- a/LICENSE +++ b/LICENSE @@ -361,7 +361,7 @@ Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. + file, You can obtain one at https://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE diff --git a/README.md b/README.md index 937967a1c..96ff5b485 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ -# Delta Chat Rust +

+ Delta Chat Logo +

-> Deltachat-core written in Rust +

+ + Rust CI + +

-[![Rust CI](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml) +

+The core library for Delta Chat, written in Rust +

## Installing Rust and Cargo @@ -167,8 +175,8 @@ Language bindings are available for: - **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\] - **Node.js** - - over cffi (legacy): \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\] - - over jsonrpc built with napi.rs: \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\] + - over cffi: \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\] + - over jsonrpc built with napi.rs (experimental): \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\] - **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\] - **Go** - over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\] diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 000000000..b9f50e9ca --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,21 @@ +# Releasing a new version of DeltaChat core + +For example, to release version 1.116.0 of the core, do the following steps. + +1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker). + +2. Run `npm run build:core:constants` in the root of the repository + and commit generated `node/constants.js`, `node/events.js` and `node/lib/constants.js`. + +3. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`. + +4. Update the version by running `scripts/set_core_version.py 1.116.0`. + +5. Commit the changes as `chore(release): prepare for 1.116.0`. + Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review. + +6. Tag the release: `git tag -a v1.116.0`. + +7. Push the release tag: `git push origin v1.116.0`. + +8. Create a GitHub release: `gh release create v1.116.0 -n ''`. diff --git a/benches/create_account.rs b/benches/create_account.rs index 5e1ae8561..c487004ac 100644 --- a/benches/create_account.rs +++ b/benches/create_account.rs @@ -8,7 +8,8 @@ async fn create_accounts(n: u32) { let dir = tempdir().unwrap(); let p: PathBuf = dir.path().join("accounts"); - let mut accounts = Accounts::new(p.clone()).await.unwrap(); + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await.unwrap(); for expected_id in 2..n { let id = accounts.add_account().await.unwrap(); diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 000000000..98a959c28 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,79 @@ +# configuration file for git-cliff +# see https://git-cliff.org/docs/configuration/ + + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = false +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/deltachat/deltachat-core-rust/pull/${2}))"}, # replace pull request / issue numbers +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "Features / Changes"}, + { message = "^fix", group = "Fixes"}, + { message = "^api", group = "API-Changes" }, + { message = "^refactor", group = "Refactor"}, + { message = "^perf", group = "Performance"}, + { message = "^test", group = "Tests"}, + { message = "^style", group = "Styling"}, + { message = "^chore\\(release\\): prepare for", skip = true}, + { message = "^chore", group = "Miscellaneous Tasks"}, + { message = "^build", group = "Build system"}, + { message = "^docs", group = "Documentation"}, + { message = "^ci", group = "CI"}, + { message = ".*", group = "Other"}, +# { body = ".*security", group = "Security"}, +] +# protect breaking changes from being skipped due to matching a skipping commit_parser +protect_breaking_commits = true +# filter out the commits that are not matched by commit parsers +filter_commits = true +# glob pattern for matching git tags +tag_pattern = "v[0-9]*" +# regex for skipping tags +#skip_tags = "v0.1.0-beta.1" +# regex for ignoring tags +ignore_tags = "" +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" +# limit the number of commits included in the changelog. +# limit_commits = 42 + + +[changelog] +# changelog header +header = """ +# Changelog\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#templates +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.breaking %}[**breaking**] {% endif %}\ + {% if commit.scope %}{{ commit.scope }}: {% endif %}\ + {{ commit.message | upper_first }}.\ + {% if commit.footers is defined %}\ + {% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %} + {% raw %} {% endraw %}- {{ footer.value }}\ + {% endif %}{% endfor %}\ + {% endif%}\ + {% endfor %} +{% endfor %}\n +""" +# remove the leading and trailing whitespace from the template +trim = true diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index f6ed211e4..fe8064f85 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat_ffi" -version = "1.112.6" +version = "1.126.1" description = "Deltachat FFI" edition = "2018" readme = "README.md" @@ -24,7 +24,8 @@ tokio = { version = "1", features = ["rt-multi-thread"] } anyhow = "1" thiserror = "1" rand = "0.8" -once_cell = "1.17.0" +once_cell = "1.18.0" +yerpc = { version = "0.5.1", features = ["anyhow_expose"] } [features] default = ["vendored"] diff --git a/deltachat-ffi/Doxyfile b/deltachat-ffi/Doxyfile index 03365989d..5c60f15e3 100644 --- a/deltachat-ffi/Doxyfile +++ b/deltachat-ffi/Doxyfile @@ -846,7 +846,7 @@ EXCLUDE_PATTERNS = # exclude all test directories use the pattern */test/* ###################################################### -EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_keyring_t dc_loginparam_t dc_mime*_t +EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_loginparam_t dc_mime*_t EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t EXCLUDE_SYMBOLS += _dc_* jsmn* ###################################################### diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index c76a5891a..ab4d746d4 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -25,6 +25,7 @@ typedef struct _dc_event dc_event_t; typedef struct _dc_event_emitter dc_event_emitter_t; typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t; typedef struct _dc_backup_provider dc_backup_provider_t; +typedef struct _dc_http_response dc_http_response_t; // Alias for backwards compatibility, use dc_event_emitter_t instead. typedef struct _dc_event_emitter dc_accounts_event_emitter_t; @@ -300,6 +301,19 @@ dc_context_t* dc_context_new_closed (const char* dbfile); int dc_context_open (dc_context_t *context, const char* passphrase); +/** + * Changes the passphrase on the open database. + * Existing database must already be encrypted and the passphrase cannot be NULL or empty. + * It is impossible to encrypt unencrypted database with this method and vice versa. + * + * @memberof dc_context_t + * @param context The context object. + * @param passphrase The new passphrase. + * @return 1 on success, 0 on error. + */ +int dc_context_change_passphrase (dc_context_t* context, const char* passphrase); + + /** * Returns 1 if database is open. * @@ -370,7 +384,12 @@ char* dc_get_blobdir (const dc_context_t* context); /** * Configure the context. The configuration is handled by key=value pairs as: * - * - `addr` = address to display (always needed) + * - `addr` = Email address to use for configuration. + * If dc_configure() fails this is not the email address actually in use. + * Use `configured_addr` to find out the email address actually in use. + * - `configured_addr` = Email address actually in use. + * Unless for testing, do not set this value using dc_set_config(). + * Instead, set `addr` and call dc_configure(). * - `mail_server` = IMAP-server, guessed if left out * - `mail_user` = IMAP-username, guessed if left out * - `mail_pw` = IMAP-password (always needed) @@ -419,17 +438,19 @@ char* dc_get_blobdir (const dc_context_t* context); * 0=watch all folders normally (default) * changes require restarting IO by calling dc_stop_io() and then dc_start_io(). * - `show_emails` = DC_SHOW_EMAILS_OFF (0)= - * show direct replies to chats only (default), + * show direct replies to chats only, * DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)= * also show all mails of confirmed contacts, * DC_SHOW_EMAILS_ALL (2)= - * also show mails of unconfirmed contacts. + * also show mails of unconfirmed contacts (default). * - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)= * generate recommended key type (default), * DC_KEY_GEN_RSA2048 (1)= * generate RSA 2048 keypair * DC_KEY_GEN_ED25519 (2)= - * generate Ed25519 keypair + * generate Curve25519 keypair + * DC_KEY_GEN_RSA4096 (3)= + * generate RSA 4096 keypair * - `save_mime_headers` = 1=save mime headers * and make dc_get_mime_headers() work for subsequent calls, * 0=do not save mime headers (default) @@ -460,8 +481,9 @@ char* dc_get_blobdir (const dc_context_t* context); * If no type is prefixed, the videochat is handled completely in a browser. * - `bot` = Set to "1" if this is a bot. * Prevents adding the "Device messages" and "Saved messages" chats, - * adds Auto-Submitted header to outgoing messages - * and accepts contact requests automatically (calling dc_accept_chat() is not needed for bots). + * adds Auto-Submitted header to outgoing messages, + * accepts contact requests automatically (calling dc_accept_chat() is not needed for bots) + * and does not cut large incoming text messages. * - `last_msg_id` = database ID of the last message processed by the bot. * This ID and IDs below it are guaranteed not to be returned * by dc_get_next_msgs() and dc_wait_next_msgs(). @@ -475,6 +497,9 @@ char* dc_get_blobdir (const dc_context_t* context); * - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default), * 0=do not fetch existing messages on configure. * In both cases, existing recipients are added to the contact database. + * - `disable_idle` = 1=disable IMAP IDLE even if the server supports it, + * 0=use IMAP IDLE if the server supports it. + * This is a developer option used for testing polling used as an IDLE fallback. * - `download_limit` = Messages up to this number of bytes are downloaded automatically. * For larger messages, only the header is downloaded and a placeholder is shown. * These messages can be downloaded fully using dc_download_full_msg() later. @@ -483,6 +508,16 @@ char* dc_get_blobdir (const dc_context_t* context); * to not mess up with non-delivery-reports or read-receipts. * 0=no limit (default). * Changes affect future messages only. + * - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in + * seconds. 2 days by default. + * This is not supposed to be changed by UIs and only used for testing. + * - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it + * to 1 if it supports verified 1:1 chats. + * Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified, + * and when the key changes, an info message is posted into the chat. + * 0=Nothing else happens when the key changes. + * 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true + * until `dc_accept_chat()` is called. * - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes. * The prefix should be followed by the system and maybe subsystem, * e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`. @@ -807,7 +842,7 @@ void dc_maybe_network (dc_context_t* context); * @param context The context as created by dc_context_new(). * @param addr The e-mail address of the user. This must match the * configured_addr setting of the context as well as the UID of the key. - * @param public_data ASCII armored public key. + * @param public_data Ignored, actual public key is extracted from secret_data. * @param secret_data ASCII armored secret key. * @return 1 on success, 0 on failure. */ @@ -861,7 +896,8 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha * - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT * is added as needed. * @param query_str An optional query for filtering the list. Only chats matching this query - * are returned. Give NULL for no filtering. + * are returned. Give NULL for no filtering. When `is:unread` is contained in the query, + * the chatlist is filtered such that only chats with unread messages show up. * @param query_id An optional contact ID for filtering the list. Only chats including this contact ID * are returned. Give 0 for no filtering. * @return A chatlist as an dc_chatlist_t object. @@ -1101,7 +1137,7 @@ dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id); * * In JS land, that would be mapped to something as: * ``` - * success = window.webxdc.sendUpdate('{"action":"move","src":"A3","dest":"B4"}', 'move A3 B4'); + * success = window.webxdc.sendUpdate('{payload: {"action":"move","src":"A3","dest":"B4"}}', 'move A3 B4'); * ``` * `context` and `msg_id` are not needed in JS as those are unique within a webxdc instance. * See dc_get_webxdc_status_updates() for the receiving counterpart. @@ -1319,6 +1355,20 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id); +/** + * Returns a list of similar chats. + * + * @warning This is an experimental API which may change or be removed in the future. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id The ID of the chat for which to find similar chats. + * @return The list of similar chats. + * On errors, NULL is returned. + * Must be freed using dc_chatlist_unref() when no longer used. + */ +dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t chat_id); + /** * Estimate the number of messages that will be deleted @@ -1450,6 +1500,7 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch * Typically used to implement the "next" and "previous" buttons * in a gallery or in a media player. * + * @deprecated Deprecated 2023-10-03, use dc_get_chat_media() and navigate the returned array instead. * @memberof dc_context_t * @param context The context object as returned from dc_context_new(). * @param msg_id The ID of the current message from which the next or previous message should be searched. @@ -1468,24 +1519,6 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3); -/** - * Enable or disable protection against active attacks. - * To enable protection, it is needed that all members are verified; - * if this condition is met, end-to-end-encryption is always enabled - * and only the verified keys are used. - * - * Sends out #DC_EVENT_CHAT_MODIFIED on changes - * and #DC_EVENT_MSGS_CHANGED if a status message was sent. - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id The ID of the chat to change the protection for. - * @param protect 1=protect chat, 0=unprotect chat - * @return 1=success, 0=error, e.g. some members may be unverified - */ -int dc_set_chat_protection (dc_context_t* context, uint32_t chat_id, int protect); - - /** * Set chat visibility to pinned, archived or normal. * @@ -1679,24 +1712,12 @@ uint32_t dc_create_group_chat (dc_context_t* context, int protect * Create a new broadcast list. * * Broadcast lists are similar to groups on the sending device, - * however, recipients get the messages in normal one-to-one chats - * and will not be aware of other members. + * however, recipients get the messages in a read-only chat + * and will see who the other members are. * - * Replies to broadcasts go only to the sender - * and not to all broadcast recipients. - * Moreover, replies will not appear in the broadcast list - * but in the one-to-one chat with the person answering. - * - * The name and the image of the broadcast list is set automatically - * and is visible to the sender only. - * Not asking for these data allows more focused creation - * and we bypass the question who will get which data. - * Also, many users will have at most one broadcast list - * so, a generic name and image is sufficient at the first place. - * - * Later on, however, the name can be changed using dc_set_chat_name(). - * The image cannot be changed to have a unique, recognizable icon in the chat lists. - * All in all, this is also what other messengers are doing here. + * For historical reasons, this function does not take a name directly, + * instead you have to set the name using dc_set_chat_name() + * after creating the broadcast list. * * @memberof dc_context_t * @param context The context object. @@ -2239,8 +2260,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co * the backup is not encrypted. * The backup contains all contacts, chats, images and other data and device independent settings. * The backup does not contain device dependent settings as ringtones or LED notification settings. - * The name of the backup is typically `delta-chat-.tar`, if more than one backup is create on a day, - * the format is `delta-chat--.tar` + * The name of the backup is `delta-chat-backup---.tar`. * * - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase. * The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup @@ -2253,6 +2273,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co * * - **DC_IMEX_IMPORT_SELF_KEYS** (2) - Import private keys found in the directory given as `param1`. * The last imported key is made the default keys unless its name contains the string `legacy`. Public keys are not imported. + * If `param1` is a filename, import the private key from the file and make it the default. * * While dc_imex() returns immediately, the started job may take a while, * you can stop it using dc_stop_ongoing_process(). During execution of the job, @@ -2924,12 +2945,15 @@ int dc_receive_backup (dc_context_t* context, const char* qr); * @param dir The directory to create the context-databases in. * If the directory does not exist, * dc_accounts_new() will try to create it. + * @param writable Whether the returned account manager is writable, i.e. calling these functions on + * it is possible: dc_accounts_add_account(), dc_accounts_add_closed_account(), + * dc_accounts_migrate_account(), dc_accounts_remove_account(), dc_accounts_select_account(). * @return An account manager object. * The object must be passed to the other account manager functions * and must be freed using dc_accounts_unref() after usage. * On errors, NULL is returned. */ -dc_accounts_t* dc_accounts_new (const char* os_name, const char* dir); +dc_accounts_t* dc_accounts_new (const char* dir, int writable); /** @@ -3710,7 +3734,6 @@ int dc_chat_can_send (const dc_chat_t* chat); * Check if a chat is protected. * Protected chats contain only verified members and encryption is always enabled. * Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1. - * The status can be changed using dc_set_chat_protection(). * * @memberof dc_chat_t * @param chat The chat object. @@ -3719,6 +3742,26 @@ int dc_chat_can_send (const dc_chat_t* chat); int dc_chat_is_protected (const dc_chat_t* chat); +/** + * Checks if the chat was protected, and then an incoming message broke this protection. + * + * This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag, + * otherwise it will return false for all chats. + * + * 1:1 chats are automatically set as protected when a contact is verified. + * When a message comes in that is not encrypted / signed correctly, + * the chat is automatically set as unprotected again. + * dc_chat_is_protection_broken() will return true until dc_accept_chat() is called. + * + * The UI should let the user confirm that this is OK with a message like + * `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat(). + * @memberof dc_chat_t + * @param chat The chat object. + * @return 1=chat protection broken, 0=otherwise. + */ +int dc_chat_is_protection_broken (const dc_chat_t* chat); + + /** * Check if locations are sent to the chat * at the time the object was created using dc_get_chat(). @@ -3923,7 +3966,7 @@ int64_t dc_msg_get_received_timestamp (const dc_msg_t* msg); * Get the message time used for sorting. * This function returns the timestamp that is used for sorting the message * into lists as returned e.g. by dc_get_chat_msgs(). - * This may be the reveived time, the sending time or another time. + * This may be the received time, the sending time or another time. * * To get the receiving time, use dc_msg_get_received_timestamp(). * To get the sending time, use dc_msg_get_timestamp(). @@ -3976,16 +4019,17 @@ char* dc_msg_get_text (const dc_msg_t* msg); */ char* dc_msg_get_subject (const dc_msg_t* msg); + /** - * Find out full path, file name and extension of the file associated with a - * message. + * Find out full path of the file associated with a message. * * Typically files are associated with images, videos, audios, documents. * Plain text messages do not have a file. + * File name may be mangled. To obtain the original attachment filename use dc_msg_get_filename(). * * @memberof dc_msg_t * @param msg The message object. - * @return The full path, the file name, and the extension of the file associated with the message. + * @return The full path (with file name and extension) of the file associated with the message. * If there is no file associated with the message, an empty string is returned. * NULL is never returned and the returned value must be released using dc_str_unref(). */ @@ -3993,14 +4037,13 @@ char* dc_msg_get_file (const dc_msg_t* msg); /** - * Get a base file name without the path. The base file name includes the extension; the path - * is not returned. To get the full path, use dc_msg_get_file(). + * Get an original attachment filename, with extension but without the path. To get the full path, + * use dc_msg_get_file(). * * @memberof dc_msg_t * @param msg The message object. - * @return The base file name plus the extension without part. If there is no file - * associated with the message, an empty string is returned. The returned - * value must be released using dc_str_unref(). + * @return The attachment filename. If there is no file associated with the message, an empty string + * is returned. The returned value must be released using dc_str_unref(). */ char* dc_msg_get_filename (const dc_msg_t* msg); @@ -4313,7 +4356,7 @@ int dc_msg_is_forwarded (const dc_msg_t* msg); * Check if the message is an informational message, created by the * device or by another users. Such messages are not "typed" by the user but * created due to other actions, - * e.g. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection() + * e.g. dc_set_chat_name(), dc_set_chat_profile_image(), * or dc_add_contact_to_chat(). * * These messages are typically shown in the center of the chat view, @@ -5007,7 +5050,12 @@ int dc_contact_is_verified (dc_contact_t* contact); /** * Return the address that verified a contact * - * The UI may use this in addition to a checkmark showing the verification status + * The UI may use this in addition to a checkmark showing the verification status. + * In case of verification chains, + * the last contact in the chain is shown. + * This is because of privacy reasons, but also as it would not help the user + * to see a unknown name here - where one can mostly always ask the shown name + * as it is directly known. * * @memberof dc_contact_t * @param contact The contact object. @@ -5015,6 +5063,7 @@ int dc_contact_is_verified (dc_contact_t* contact); * A string containing the verifiers address. If it is the same address as the contact itself, * we verified the contact ourself. If it is an empty string, we don't have verifier * information or the contact is not verified. + * @deprecated 2023-09-28, use dc_contact_get_verifier_id instead */ char* dc_contact_get_verifier_addr (dc_contact_t* contact); @@ -5027,7 +5076,7 @@ char* dc_contact_get_verifier_addr (dc_contact_t* contact); * @memberof dc_contact_t * @param contact The contact object. * @return - * The `ContactId` of the verifiers address. If it is the same address as the contact itself, + * The contact ID of the verifier. If it is DC_CONTACT_ID_SELF, * we verified the contact ourself. If it is 0, we don't have verifier information or * the contact is not verified. */ @@ -5127,6 +5176,72 @@ int dc_provider_get_status (const dc_provider_t* prov void dc_provider_unref (dc_provider_t* provider); +/** + * Return an HTTP(S) GET response. + * This function can be used to download remote content for HTML emails. + * + * @memberof dc_context_t + * @param context The context object to take proxy settings from. + * @param url HTTP or HTTPS URL. + * @return The response must be released using dc_http_response_unref() after usage. + * NULL is returned on errors. + */ +dc_http_response_t* dc_get_http_response (const dc_context_t* context, const char* url); + + +/** + * @class dc_http_response_t + * + * An object containing an HTTP(S) GET response. + * Created by dc_get_http_response(). + */ + + +/** + * Returns HTTP response MIME type as a string, e.g. "text/plain" or "text/html". + * + * @memberof dc_http_response_t + * @param response HTTP response as returned by dc_get_http_response(). + * @return The string which must be released using dc_str_unref() after usage. May be NULL. + */ +char* dc_http_response_get_mimetype (const dc_http_response_t* response); + +/** + * Returns HTTP response encoding, e.g. "utf-8". + * + * @memberof dc_http_response_t + * @param response HTTP response as returned by dc_get_http_response(). + * @return The string which must be released using dc_str_unref() after usage. May be NULL. + */ +char* dc_http_response_get_encoding (const dc_http_response_t* response); + +/** + * Returns HTTP response contents. + * + * @memberof dc_http_response_t + * @param response HTTP response as returned by dc_get_http_response(). + * @return The blob which must be released using dc_str_unref() after usage. NULL is never returned. + */ +uint8_t* dc_http_response_get_blob (const dc_http_response_t* response); + +/** + * Returns HTTP response content size. + * + * @memberof dc_http_response_t + * @param response HTTP response as returned by dc_get_http_response(). + * @return The blob size. + */ +size_t dc_http_response_get_size (const dc_http_response_t* response); + +/** + * Free an HTTP response object. + * + * @memberof dc_http_response_t + * @param response HTTP response as returned by dc_get_http_response(). + */ +void dc_http_response_unref (const dc_http_response_t* response); + + /** * @class dc_lot_t * @@ -5604,7 +5719,6 @@ void dc_reactions_unref (dc_reactions_t* reactions); */ - /** * @class dc_jsonrpc_instance_t * @@ -5653,6 +5767,17 @@ void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, const char* req */ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance); +/** + * Make a JSON-RPC call and return a response. + * + * @memberof dc_jsonrpc_instance_t + * @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init(). + * @param input JSON-RPC request. + * @return JSON-RPC response as string, must be freed using dc_str_unref() after usage. + * If there is no response, NULL is returned. + */ +char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input); + /** * @class dc_event_emitter_t * @@ -6000,6 +6125,15 @@ void dc_event_unref(dc_event_t* event); #define DC_EVENT_MSG_READ 2015 +/** + * A single message is deleted. + * + * @param data1 (int) chat_id + * @param data2 (int) msg_id + */ +#define DC_EVENT_MSG_DELETED 2016 + + /** * Chat changed. The name or the image of a chat group was changed or members were added or removed. * Or the verify state of a chat has changed. @@ -6178,6 +6312,7 @@ void dc_event_unref(dc_event_t* event); #define DC_KEY_GEN_DEFAULT 0 #define DC_KEY_GEN_RSA2048 1 #define DC_KEY_GEN_ED25519 2 +#define DC_KEY_GEN_RSA4096 3 /** @@ -6661,15 +6796,6 @@ void dc_event_unref(dc_event_t* event); /// Used in error strings. #define DC_STR_ERROR_NO_NETWORK 87 -/// "Chat protection enabled." -/// - -/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY. -#define DC_STR_PROTECTION_ENABLED 88 - -/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY. -#define DC_STR_PROTECTION_DISABLED 89 - /// "Reply" /// /// Used in summaries. @@ -7114,26 +7240,6 @@ void dc_event_unref(dc_event_t* event); /// `%2$s` will be replaced by name and address of the contact. #define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157 -/// "You enabled chat protection." -/// -/// Used in status messages. -#define DC_STR_PROTECTION_ENABLED_BY_YOU 158 - -/// "Chat protection enabled by %1$s." -/// -/// `%1$s` will be replaced by name and address of the contact. -/// -/// Used in status messages. -#define DC_STR_PROTECTION_ENABLED_BY_OTHER 159 - -/// "You disabled chat protection." -#define DC_STR_PROTECTION_DISABLED_BY_YOU 160 - -/// "Chat protection disabled by %1$s." -/// -/// `%1$s` will be replaced by name and address of the contact. -#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161 - /// "Scan to set up second device for %1$s" /// /// `%1$s` will be replaced by name and address of the account. @@ -7144,6 +7250,16 @@ void dc_event_unref(dc_event_t* event); /// Used as a device message after a successful backup transfer. #define DC_STR_BACKUP_TRANSFER_MSG_BODY 163 +/// "Messages are guaranteed to be end-to-end encrypted from now on." +/// +/// Used in info messages. +#define DC_STR_CHAT_PROTECTION_ENABLED 170 + +/// "%1$s sent a message from another device." +/// +/// Used in info messages. +#define DC_STR_CHAT_PROTECTION_DISABLED 171 + /** * @} */ diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 6872c6d38..025c3f7ce 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -29,8 +29,9 @@ use deltachat::contact::{Contact, ContactId, Origin}; use deltachat::context::Context; use deltachat::ephemeral::Timer as EphemeralTimer; use deltachat::imex::BackupProvider; -use deltachat::key::DcKey; +use deltachat::key::preconfigure_keypair; use deltachat::message::MsgId; +use deltachat::net::read_url_blob; use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg}; use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions}; use deltachat::stock_str::StockMessage; @@ -166,6 +167,24 @@ pub unsafe extern "C" fn dc_context_open( .unwrap_or(0) } +#[no_mangle] +pub unsafe extern "C" fn dc_context_change_passphrase( + context: *mut dc_context_t, + passphrase: *const libc::c_char, +) -> libc::c_int { + if context.is_null() { + eprintln!("ignoring careless call to dc_context_change_passphrase()"); + return 0; + } + + let ctx = &*context; + let passphrase = to_string_lossy(passphrase); + block_on(ctx.change_passphrase(passphrase)) + .context("dc_context_change_passphrase() failed") + .log_err(ctx) + .is_ok() as libc::c_int +} + #[no_mangle] pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int { if context.is_null() { @@ -526,6 +545,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int EventType::MsgDelivered { .. } => 2010, EventType::MsgFailed { .. } => 2012, EventType::MsgRead { .. } => 2015, + EventType::MsgDeleted { .. } => 2016, EventType::ChatModified(_) => 2020, EventType::ChatEphemeralTimerModified { .. } => 2021, EventType::ContactsChanged(_) => 2030, @@ -573,6 +593,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc: | EventType::MsgDelivered { chat_id, .. } | EventType::MsgFailed { chat_id, .. } | EventType::MsgRead { chat_id, .. } + | EventType::MsgDeleted { chat_id, .. } | EventType::ChatModified(chat_id) | EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int, EventType::ContactsChanged(id) | EventType::LocationChanged(id) => { @@ -630,7 +651,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc: | EventType::IncomingMsg { msg_id, .. } | EventType::MsgDelivered { msg_id, .. } | EventType::MsgFailed { msg_id, .. } - | EventType::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int, + | EventType::MsgRead { msg_id, .. } + | EventType::MsgDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int, EventType::SecurejoinInviterProgress { progress, .. } | EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int, EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int, @@ -673,6 +695,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut | EventType::MsgDelivered { .. } | EventType::MsgFailed { .. } | EventType::MsgRead { .. } + | EventType::MsgDeleted { .. } | EventType::ChatModified(_) | EventType::ContactsChanged(_) | EventType::LocationChanged(_) @@ -782,7 +805,7 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) { pub unsafe extern "C" fn dc_preconfigure_keypair( context: *mut dc_context_t, addr: *const libc::c_char, - public_data: *const libc::c_char, + _public_data: *const libc::c_char, secret_data: *const libc::c_char, ) -> i32 { if context.is_null() { @@ -790,21 +813,12 @@ pub unsafe extern "C" fn dc_preconfigure_keypair( return 0; } let ctx = &*context; - block_on(async move { - let addr = tools::EmailAddress::new(&to_string_lossy(addr))?; - let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0; - let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0; - let keypair = key::KeyPair { - addr, - public, - secret, - }; - key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default).await?; - Ok::<_, anyhow::Error>(1) - }) - .context("Failed to save keypair") - .log_err(ctx) - .unwrap_or(0) + let addr = to_string_lossy(addr); + let secret_data = to_string_lossy(secret_data); + block_on(preconfigure_keypair(ctx, &addr, &secret_data)) + .context("Failed to save keypair") + .log_err(ctx) + .is_ok() as libc::c_int } #[no_mangle] @@ -1237,6 +1251,30 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt( }) } +#[no_mangle] +pub unsafe extern "C" fn dc_get_similar_chatlist( + context: *mut dc_context_t, + chat_id: u32, +) -> *mut dc_chatlist_t { + if context.is_null() { + eprintln!("ignoring careless call to dc_get_similar_chatlist()"); + return ptr::null_mut(); + } + let ctx = &*context; + + let chat_id = ChatId::new(chat_id); + match block_on(chat_id.get_similar_chatlist(ctx)) + .context("failed to get similar chatlist") + .log_err(ctx) + { + Ok(list) => { + let ffi_list = ChatlistWrapper { context, list }; + Box::into_raw(Box::new(ffi_list)) + } + Err(_) => ptr::null_mut(), + } +} + #[no_mangle] pub unsafe extern "C" fn dc_estimate_deletion_cnt( context: *mut dc_context_t, @@ -1384,6 +1422,7 @@ pub unsafe extern "C" fn dc_get_chat_media( } #[no_mangle] +#[allow(deprecated)] pub unsafe extern "C" fn dc_get_next_media( context: *mut dc_context_t, msg_id: u32, @@ -1424,32 +1463,6 @@ pub unsafe extern "C" fn dc_get_next_media( }) } -#[no_mangle] -pub unsafe extern "C" fn dc_set_chat_protection( - context: *mut dc_context_t, - chat_id: u32, - protect: libc::c_int, -) -> libc::c_int { - if context.is_null() { - eprintln!("ignoring careless call to dc_set_chat_protection()"); - return 0; - } - let ctx = &*context; - let protect = if let Some(s) = ProtectionStatus::from_i32(protect) { - s - } else { - warn!(ctx, "bad protect-value for dc_set_chat_protection()"); - return 0; - }; - - block_on(async move { - match ChatId::new(chat_id).set_protection(ctx, protect).await { - Ok(()) => 1, - Err(_) => 0, - } - }) -} - #[no_mangle] pub unsafe extern "C" fn dc_set_chat_visibility( context: *mut dc_context_t, @@ -1872,13 +1885,10 @@ pub unsafe extern "C" fn dc_get_msg_info( return "".strdup(); } let ctx = &*context; - - block_on(async move { - message::get_msg_info(ctx, MsgId::new(msg_id)) - .await - .unwrap_or_log_default(ctx, "failed to get msg id") - .strdup() - }) + let msg_id = MsgId::new(msg_id); + block_on(msg_id.get_info(ctx)) + .unwrap_or_log_default(ctx, "failed to get msg id") + .strdup() } #[no_mangle] @@ -2523,7 +2533,12 @@ pub unsafe extern "C" fn dc_set_location( } let ctx = &*context; - block_on(location::set(ctx, latitude, longitude, accuracy)) as _ + block_on(async move { + location::set(ctx, latitude, longitude, accuracy) + .await + .log_err(ctx) + .unwrap_or_default() + }) as libc::c_int } #[no_mangle] @@ -3082,6 +3097,16 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i ffi_chat.chat.is_protected() as libc::c_int } +#[no_mangle] +pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int { + if chat.is_null() { + eprintln!("ignoring careless call to dc_chat_is_protection_broken()"); + return 0; + } + let ffi_chat = &*chat; + ffi_chat.chat.is_protection_broken() as libc::c_int +} + #[no_mangle] pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int { if chat.is_null() { @@ -3303,7 +3328,7 @@ pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_cha return "".strdup(); } let ffi_msg = &*msg; - ffi_msg.message.get_text().unwrap_or_default().strdup() + ffi_msg.message.get_text().strdup() } #[no_mangle] @@ -3688,7 +3713,7 @@ pub unsafe extern "C" fn dc_msg_set_text(msg: *mut dc_msg_t, text: *const libc:: return; } let ffi_msg = &mut *msg; - ffi_msg.message.set_text(to_opt_string_lossy(text)) + ffi_msg.message.set_text(to_string_lossy(text)) } #[no_mangle] @@ -4487,7 +4512,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email( let ctx = &*context; - match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) { + match block_on(provider::get_provider_info_by_addr( + ctx, + addr.as_str(), + true, + )) + .log_err(ctx) + .unwrap_or_default() + { Some(provider) => provider, None => ptr::null_mut(), } @@ -4514,11 +4546,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns( match socks5_enabled { Ok(socks5_enabled) => { - match block_on(provider::get_provider_info( + match block_on(provider::get_provider_info_by_addr( ctx, addr.as_str(), socks5_enabled, - )) { + )) + .log_err(ctx) + .unwrap_or_default() + { Some(provider) => provider, None => ptr::null_mut(), } @@ -4572,6 +4607,96 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) { // this may change once we start localizing string. } +// dc_http_response_t + +pub type dc_http_response_t = net::HttpResponse; + +#[no_mangle] +pub unsafe extern "C" fn dc_get_http_response( + context: *const dc_context_t, + url: *const libc::c_char, +) -> *mut dc_http_response_t { + if context.is_null() || url.is_null() { + eprintln!("ignoring careless call to dc_get_http_response()"); + return ptr::null_mut(); + } + + let context = &*context; + let url = to_string_lossy(url); + if let Ok(response) = block_on(read_url_blob(context, &url)) + .context("read_url_blob") + .log_err(context) + { + Box::into_raw(Box::new(response)) + } else { + ptr::null_mut() + } +} + +#[no_mangle] +pub unsafe extern "C" fn dc_http_response_get_mimetype( + response: *const dc_http_response_t, +) -> *mut libc::c_char { + if response.is_null() { + eprintln!("ignoring careless call to dc_http_response_get_mimetype()"); + return ptr::null_mut(); + } + + let response = &*response; + response.mimetype.strdup() +} + +#[no_mangle] +pub unsafe extern "C" fn dc_http_response_get_encoding( + response: *const dc_http_response_t, +) -> *mut libc::c_char { + if response.is_null() { + eprintln!("ignoring careless call to dc_http_response_get_encoding()"); + return ptr::null_mut(); + } + + let response = &*response; + response.encoding.strdup() +} + +#[no_mangle] +pub unsafe extern "C" fn dc_http_response_get_blob( + response: *const dc_http_response_t, +) -> *mut libc::c_char { + if response.is_null() { + eprintln!("ignoring careless call to dc_http_response_get_blob()"); + return ptr::null_mut(); + } + + let response = &*response; + let blob_len = response.blob.len(); + let ptr = libc::malloc(blob_len); + libc::memcpy(ptr, response.blob.as_ptr() as *mut libc::c_void, blob_len); + ptr as *mut libc::c_char +} + +#[no_mangle] +pub unsafe extern "C" fn dc_http_response_get_size( + response: *const dc_http_response_t, +) -> libc::size_t { + if response.is_null() { + eprintln!("ignoring careless call to dc_http_response_get_size()"); + return 0; + } + + let response = &*response; + response.blob.len() +} + +#[no_mangle] +pub unsafe extern "C" fn dc_http_response_unref(response: *mut dc_http_response_t) { + if response.is_null() { + eprintln!("ignoring careless call to dc_http_response_unref()"); + return; + } + drop(Box::from_raw(response)); +} + // -- Accounts /// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using @@ -4600,17 +4725,17 @@ pub type dc_accounts_t = AccountsWrapper; #[no_mangle] pub unsafe extern "C" fn dc_accounts_new( - _os_name: *const libc::c_char, - dbfile: *const libc::c_char, + dir: *const libc::c_char, + writable: libc::c_int, ) -> *mut dc_accounts_t { setup_panic!(); - if dbfile.is_null() { + if dir.is_null() { eprintln!("ignoring careless call to dc_accounts_new()"); return ptr::null_mut(); } - let accs = block_on(Accounts::new(as_path(dbfile).into())); + let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0)); match accs { Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))), @@ -4876,7 +5001,6 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter( #[cfg(feature = "jsonrpc")] mod jsonrpc { use deltachat_jsonrpc::api::CommandApi; - use deltachat_jsonrpc::events::event_to_json_rpc_notification; use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession}; use super::*; @@ -4884,7 +5008,6 @@ mod jsonrpc { pub struct dc_jsonrpc_instance_t { receiver: OutReceiver, handle: RpcSession, - event_thread: JoinHandle>, } #[no_mangle] @@ -4897,28 +5020,12 @@ mod jsonrpc { } let account_manager = &*account_manager; - let events = block_on(account_manager.read()).get_event_emitter(); let cmd_api = deltachat_jsonrpc::api::CommandApi::from_arc(account_manager.inner.clone()); let (request_handle, receiver) = RpcClient::new(); - let handle = RpcSession::new(request_handle.clone(), cmd_api); + let handle = RpcSession::new(request_handle, cmd_api); - let event_thread = spawn(async move { - while let Some(event) = events.recv().await { - let event = event_to_json_rpc_notification(event); - request_handle - .send_notification("event", Some(event)) - .await?; - } - let res: Result<(), anyhow::Error> = Ok(()); - res - }); - - let instance = dc_jsonrpc_instance_t { - receiver, - handle, - event_thread, - }; + let instance = dc_jsonrpc_instance_t { receiver, handle }; Box::into_raw(Box::new(instance)) } @@ -4929,7 +5036,6 @@ mod jsonrpc { eprintln!("ignoring careless call to dc_jsonrpc_unref()"); return; } - (*jsonrpc_instance).event_thread.abort(); drop(Box::from_raw(jsonrpc_instance)); } @@ -4967,4 +5073,28 @@ mod jsonrpc { .map(|result| serde_json::to_string(&result).unwrap_or_default().strdup()) .unwrap_or(ptr::null_mut()) } + + #[no_mangle] + pub unsafe extern "C" fn dc_jsonrpc_blocking_call( + jsonrpc_instance: *mut dc_jsonrpc_instance_t, + input: *const libc::c_char, + ) -> *mut libc::c_char { + if jsonrpc_instance.is_null() { + eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()"); + return ptr::null_mut(); + } + let api = &*jsonrpc_instance; + let input = to_string_lossy(input); + let res = block_on(api.handle.process_incoming(&input)); + match res { + Some(message) => { + if let Ok(message) = serde_json::to_string(&message) { + message.strdup() + } else { + ptr::null_mut() + } + } + None => ptr::null_mut(), + } + } } diff --git a/deltachat-jsonrpc/.gitignore b/deltachat-jsonrpc/.gitignore index c12c4a8ba..33653476a 100644 --- a/deltachat-jsonrpc/.gitignore +++ b/deltachat-jsonrpc/.gitignore @@ -1,3 +1,4 @@ +openrpc/openrpc.json accounts/ .cargo \ No newline at end of file diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index 77787abd3..ec2900f09 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-jsonrpc" -version = "1.112.6" +version = "1.126.1" description = "DeltaChat JSON-RPC API" edition = "2021" default-run = "deltachat-jsonrpc-server" @@ -15,25 +15,26 @@ required-features = ["webserver"] anyhow = "1" deltachat = { path = ".." } num-traits = "0.2" +schemars = "0.8.13" serde = { version = "1.0", features = ["derive"] } -tempfile = "3.3.0" +tempfile = "3.8.0" log = "0.4" async-channel = { version = "1.8.0" } futures = { version = "0.3.28" } -serde_json = "1.0.95" -yerpc = { version = "0.4.3", features = ["anyhow_expose"] } -typescript-type-def = { version = "0.5.5", features = ["json_value"] } -tokio = { version = "1.27.0" } -sanitize-filename = "0.4" +serde_json = "1.0.105" +yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] } +typescript-type-def = { version = "0.5.8", features = ["json_value"] } +tokio = { version = "1.32.0" } +sanitize-filename = "0.5" walkdir = "2.3.3" base64 = "0.21" # optional dependencies -axum = { version = "0.6.12", optional = true, features = ["ws"] } +axum = { version = "0.6.20", optional = true, features = ["ws"] } env_logger = { version = "0.10.0", optional = true } [dev-dependencies] -tokio = { version = "1.27.0", features = ["full", "rt-multi-thread"] } +tokio = { version = "1.32.0", features = ["full", "rt-multi-thread"] } [features] diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index cc994ed63..b67c8e595 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -4,48 +4,47 @@ use std::{collections::HashMap, str::FromStr}; use anyhow::{anyhow, bail, ensure, Context, Result}; pub use deltachat::accounts::Accounts; -use deltachat::qr::Qr; -use deltachat::{ - chat::{ - self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex, - marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions, - ProtectionStatus, - }, - chatlist::Chatlist, - config::Config, - constants::DC_MSG_ID_DAYMARKER, - contact::{may_be_valid_addr, Contact, ContactId, Origin}, - context::get_info, - ephemeral::Timer, - imex, location, - message::{ - self, delete_msgs, get_msg_info, markseen_msgs, Message, MessageState, MsgId, Viewtype, - }, - provider::get_provider_info, - qr, - qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg}, - reaction::send_reaction, - securejoin, - stock_str::StockMessage, - webxdc::StatusUpdateSerial, +use deltachat::chat::{ + self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex, + marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions, + ProtectionStatus, }; +use deltachat::chatlist::Chatlist; +use deltachat::config::Config; +use deltachat::constants::DC_MSG_ID_DAYMARKER; +use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin}; +use deltachat::context::get_info; +use deltachat::ephemeral::Timer; +use deltachat::imex; +use deltachat::location; +use deltachat::message::get_msg_read_receipts; +use deltachat::message::{ + self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype, +}; +use deltachat::provider::get_provider_info; +use deltachat::qr::{self, Qr}; +use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg}; +use deltachat::reaction::{get_msg_reactions, send_reaction}; +use deltachat::securejoin; +use deltachat::stock_str::StockMessage; +use deltachat::webxdc::StatusUpdateSerial; use sanitize_filename::is_sanitized; use tokio::fs; use tokio::sync::{watch, Mutex, RwLock}; use walkdir::WalkDir; use yerpc::rpc; -pub mod events; pub mod types; use num_traits::FromPrimitive; use types::account::Account; use types::chat::FullChat; -use types::chat_list::ChatListEntry; use types::contact::ContactObject; -use types::message::MessageData; -use types::message::MessageObject; +use types::events::Event; +use types::http::HttpResponse; +use types::message::{MessageData, MessageObject, MessageReadReceipt}; use types::provider_info::ProviderInfo; +use types::reactions::JSONRPCReactions; use types::webxdc::WebxdcMessageInfo; use self::types::message::MessageLoadResult; @@ -154,16 +153,26 @@ impl CommandApi { // Misc top level functions // --------------------------------------------- - /// Check if an email address is valid. + /// Checks if an email address is valid. async fn check_email_validity(&self, email: String) -> bool { may_be_valid_addr(&email) } - /// Get general system info. + /// Returns general system info. async fn get_system_info(&self) -> BTreeMap<&'static str, String> { get_info() } + /// Get the next event. + async fn get_next_event(&self) -> Result { + let event_emitter = self.accounts.read().await.get_event_emitter(); + event_emitter + .recv() + .await + .map(|event| event.into()) + .context("event channel is closed") + } + // --------------------------------------------- // Account Management // --------------------------------------------- @@ -205,18 +214,18 @@ impl CommandApi { let context_option = self.accounts.read().await.get_account(id); if let Some(ctx) = context_option { accounts.push(Account::from_context(&ctx, id).await?) - } else { - println!("account with id {id} doesn't exist anymore"); } } Ok(accounts) } + /// Starts background tasks for all accounts. async fn start_io_for_all_accounts(&self) -> Result<()> { self.accounts.read().await.start_io().await; Ok(()) } + /// Stops background tasks for all accounts. async fn stop_io_for_all_accounts(&self) -> Result<()> { self.accounts.read().await.stop_io().await; Ok(()) @@ -226,14 +235,16 @@ impl CommandApi { // Methods that work on individual accounts // --------------------------------------------- - async fn start_io(&self, id: u32) -> Result<()> { - let ctx = self.get_context(id).await?; + /// Starts background tasks for a single account. + async fn start_io(&self, account_id: u32) -> Result<()> { + let ctx = self.get_context(account_id).await?; ctx.start_io().await; Ok(()) } - async fn stop_io(&self, id: u32) -> Result<()> { - let ctx = self.get_context(id).await?; + /// Stops background tasks for a single account. + async fn stop_io(&self, account_id: u32) -> Result<()> { + let ctx = self.get_context(account_id).await?; ctx.stop_io().await; Ok(()) } @@ -300,11 +311,13 @@ impl CommandApi { ctx.get_info().await } + /// Sets the given configuration key. async fn set_config(&self, account_id: u32, key: String, value: Option) -> Result<()> { let ctx = self.get_context(account_id).await?; set_config(&ctx, &key, value.as_deref()).await } + /// Updates a batch of configuration values. async fn batch_set_config( &self, account_id: u32, @@ -336,6 +349,7 @@ impl CommandApi { Ok(qr_object) } + /// Returns configuration value for the given key. async fn get_config(&self, account_id: u32, key: String) -> Result> { let ctx = self.get_context(account_id).await?; get_config(&ctx, &key).await @@ -539,7 +553,7 @@ impl CommandApi { list_flags: Option, query_string: Option, query_contact_id: Option, - ) -> Result> { + ) -> Result> { let ctx = self.get_context(account_id).await?; let list = Chatlist::try_load( &ctx, @@ -548,32 +562,43 @@ impl CommandApi { query_contact_id.map(ContactId::new), ) .await?; - let mut l: Vec = Vec::with_capacity(list.len()); + let mut l: Vec = Vec::with_capacity(list.len()); for i in 0..list.len() { - l.push(ChatListEntry( - list.get_chat_id(i)?.to_u32(), - list.get_msg_id(i)?.unwrap_or_default().to_u32(), - )); + l.push(list.get_chat_id(i)?.to_u32()); } Ok(l) } + /// Returns chats similar to the given one. + /// + /// Experimental API, subject to change without notice. + async fn get_similar_chat_ids(&self, account_id: u32, chat_id: u32) -> Result> { + let ctx = self.get_context(account_id).await?; + let chat_id = ChatId::new(chat_id); + let list = chat_id + .get_similar_chat_ids(&ctx) + .await? + .into_iter() + .map(|(chat_id, _metric)| chat_id.to_u32()) + .collect(); + Ok(list) + } + async fn get_chatlist_items_by_entries( &self, account_id: u32, - entries: Vec, + entries: Vec, ) -> Result> { - // todo custom json deserializer for ChatListEntry? let ctx = self.get_context(account_id).await?; let mut result: HashMap = HashMap::with_capacity(entries.len()); - for entry in entries.iter() { + for &entry in entries.iter() { result.insert( - entry.0, + entry, match get_chat_list_item_by_id(&ctx, entry).await { Ok(res) => res, Err(err) => ChatListItemFetchResult::Error { - id: entry.0, + id: entry, error: format!("{err:#}"), }, }, @@ -790,24 +815,12 @@ impl CommandApi { /// Create a new broadcast list. /// /// Broadcast lists are similar to groups on the sending device, - /// however, recipients get the messages in normal one-to-one chats - /// and will not be aware of other members. + /// however, recipients get the messages in a read-only chat + /// and will see who the other members are. /// - /// Replies to broadcasts go only to the sender - /// and not to all broadcast recipients. - /// Moreover, replies will not appear in the broadcast list - /// but in the one-to-one chat with the person answering. - /// - /// The name and the image of the broadcast list is set automatically - /// and is visible to the sender only. - /// Not asking for these data allows more focused creation - /// and we bypass the question who will get which data. - /// Also, many users will have at most one broadcast list - /// so, a generic name and image is sufficient at the first place. - /// - /// Later on, however, the name can be changed using dc_set_chat_name(). - /// The image cannot be changed to have a unique, recognizable icon in the chat lists. - /// All in all, this is also what other messengers are doing here. + /// For historical reasons, this function does not take a name directly, + /// instead you have to set the name using dc_set_chat_name() + /// after creating the broadcast list. async fn create_broadcast_list(&self, account_id: u32) -> Result { let ctx = self.get_context(account_id).await?; chat::create_broadcast_list(&ctx) @@ -892,7 +905,7 @@ impl CommandApi { ) -> Result { let ctx = self.get_context(account_id).await?; let mut msg = Message::new(Viewtype::Text); - msg.set_text(Some(text)); + msg.set_text(text); let message_id = deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?; Ok(message_id.to_u32()) @@ -1110,7 +1123,25 @@ impl CommandApi { /// max. text returned by dc_msg_get_text() (about 30000 characters). async fn get_message_info(&self, account_id: u32, message_id: u32) -> Result { let ctx = self.get_context(account_id).await?; - get_msg_info(&ctx, MsgId::new(message_id)).await + MsgId::new(message_id).get_info(&ctx).await + } + + /// Returns contacts that sent read receipts and the time of reading. + async fn get_message_read_receipts( + &self, + account_id: u32, + message_id: u32, + ) -> Result> { + let ctx = self.get_context(account_id).await?; + let receipts = get_msg_read_receipts(&ctx, MsgId::new(message_id)) + .await? + .iter() + .map(|(contact_id, ts)| MessageReadReceipt { + contact_id: contact_id.to_u32(), + timestamp: *ts, + }) + .collect(); + Ok(receipts) } /// Asks the core to start downloading a message fully. @@ -1313,7 +1344,7 @@ impl CommandApi { ) -> Result<()> { let ctx = self.get_context(account_id).await?; let contact_id = ContactId::new(contact_id); - let contact = Contact::load_from_db(&ctx, contact_id).await?; + let contact = Contact::get_by_id(&ctx, contact_id).await?; let addr = contact.get_addr(); Contact::create(&ctx, &name, addr).await?; Ok(()) @@ -1387,6 +1418,10 @@ impl CommandApi { /// /// one combined call for getting chat::get_next_media for both directions /// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet + /// + /// Deprecated 2023-10-03, use `get_chat_media` method + /// and navigate the returned array instead. + #[allow(deprecated)] async fn get_neighboring_chat_media( &self, account_id: u32, @@ -1658,6 +1693,15 @@ impl CommandApi { Ok(general_purpose::STANDARD_NO_PAD.encode(blob)) } + /// Makes an HTTP GET request and returns a response. + /// + /// `url` is the HTTP or HTTPS URL. + async fn get_http_response(&self, account_id: u32, url: String) -> Result { + let ctx = self.get_context(account_id).await?; + let response = deltachat::net::read_url_blob(&ctx, &url).await?.into(); + Ok(response) + } + /// Forward messages to another chat. /// /// All types of messages can be forwarded, @@ -1675,6 +1719,20 @@ impl CommandApi { forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await } + /// Resend messages and make information available for newly added chat members. + /// Resending sends out the original message, however, recipients and webxdc-status may differ. + /// Clients that already have the original message can still ignore the resent message as + /// they have tracked the state by dedicated updates. + /// + /// Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF. + /// + /// message_ids all message IDs that should be resend. All messages must belong to the same chat. + async fn resend_messages(&self, account_id: u32, message_ids: Vec) -> Result<()> { + let ctx = self.get_context(account_id).await?; + let message_ids: Vec = message_ids.into_iter().map(MsgId::new).collect(); + chat::resend_msgs(&ctx, &message_ids).await + } + async fn send_sticker( &self, account_id: u32, @@ -1686,6 +1744,9 @@ impl CommandApi { let mut msg = Message::new(Viewtype::Sticker); msg.set_file(&sticker_path, None); + // JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image] + msg.force_sticker(); + let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?; Ok(message_id.to_u32()) } @@ -1707,6 +1768,21 @@ impl CommandApi { Ok(message_id.to_u32()) } + /// Returns reactions to the message. + async fn get_message_reactions( + &self, + account_id: u32, + message_id: u32, + ) -> Result> { + let ctx = self.get_context(account_id).await?; + let reactions = get_msg_reactions(&ctx, MsgId::new(message_id)).await?; + if reactions.is_empty() { + Ok(None) + } else { + Ok(Some(reactions.into())) + } + } + async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result { let ctx = self.get_context(account_id).await?; let mut message = Message::new(if let Some(viewtype) = data.viewtype { @@ -1716,9 +1792,7 @@ impl CommandApi { } else { Viewtype::Text }); - if data.text.is_some() { - message.set_text(data.text); - } + message.set_text(data.text.unwrap_or_default()); if data.html.is_some() { message.set_html(data.html); } @@ -1806,7 +1880,7 @@ impl CommandApi { .context("path conversion to string failed") } - /// save a sticker to a collection/folder in the account's sticker folder + /// Saves a sticker to a collection/folder in the account's sticker folder. async fn misc_save_sticker( &self, account_id: u32, @@ -1899,7 +1973,7 @@ impl CommandApi { let ctx = self.get_context(account_id).await?; let mut msg = Message::new(Viewtype::Text); - msg.set_text(Some(text)); + msg.set_text(text); let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?; Ok(message_id.to_u32()) @@ -1922,9 +1996,7 @@ impl CommandApi { } else { Viewtype::Text }); - if text.is_some() { - message.set_text(text); - } + message.set_text(text.unwrap_or_default()); if let Some(file) = file { message.set_file(file, None); } @@ -1968,9 +2040,7 @@ impl CommandApi { } else { Viewtype::Text }); - if text.is_some() { - draft.set_text(text); - } + draft.set_text(text.unwrap_or_default()); if let Some(file) = file { draft.set_file(file, None); } @@ -1989,6 +2059,23 @@ impl CommandApi { ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await } + + // send the chat's current set draft + async fn misc_send_draft(&self, account_id: u32, chat_id: u32) -> Result { + let ctx = self.get_context(account_id).await?; + if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? { + let mut draft = draft; + let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut draft) + .await? + .to_u32(); + Ok(msg_id) + } else { + Err(anyhow!( + "chat with id {} doesn't have draft message", + chat_id + )) + } + } } // Helper functions (to prevent code duplication) diff --git a/deltachat-jsonrpc/src/api/types/account.rs b/deltachat-jsonrpc/src/api/types/account.rs index 8e0f80aef..b2909109d 100644 --- a/deltachat-jsonrpc/src/api/types/account.rs +++ b/deltachat-jsonrpc/src/api/types/account.rs @@ -6,8 +6,8 @@ use typescript_type_def::TypeDef; use super::color_int_to_hex_string; -#[derive(Serialize, TypeDef)] -#[serde(tag = "type")] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] +#[serde(tag = "kind")] pub enum Account { #[serde(rename_all = "camelCase")] Configured { diff --git a/deltachat-jsonrpc/src/api/types/chat.rs b/deltachat-jsonrpc/src/api/types/chat.rs index f5a5c3bcd..e2146c8c1 100644 --- a/deltachat-jsonrpc/src/api/types/chat.rs +++ b/deltachat-jsonrpc/src/api/types/chat.rs @@ -1,6 +1,6 @@ use std::time::{Duration, SystemTime}; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Context as _, Result}; use deltachat::chat::{self, get_chat_contacts, ChatVisibility}; use deltachat::chat::{Chat, ChatId}; use deltachat::constants::Chattype; @@ -13,7 +13,7 @@ use typescript_type_def::TypeDef; use super::color_int_to_hex_string; use super::contact::ContactObject; -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename_all = "camelCase")] pub struct FullChat { id: u32, @@ -53,7 +53,9 @@ impl FullChat { contacts.push( ContactObject::try_from_dc_contact( context, - Contact::load_from_db(context, *contact_id).await?, + Contact::get_by_id(context, *contact_id) + .await + .context("failed to load contact")?, ) .await?, ) @@ -72,8 +74,9 @@ impl FullChat { let was_seen_recently = if chat.get_type() == Chattype::Single { match contact_ids.get(0) { - Some(contact) => Contact::load_from_db(context, *contact) - .await? + Some(contact) => Contact::get_by_id(context, *contact) + .await + .context("failed to load contact for was_seen_recently")? .was_seen_recently(), None => false, } @@ -89,10 +92,7 @@ impl FullChat { is_protected: chat.is_protected(), profile_image, //BLOBS ? archived: chat.get_visibility() == chat::ChatVisibility::Archived, - chat_type: chat - .get_type() - .to_u32() - .ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap? + chat_type: chat.get_type().to_u32().context("unknown chat type id")?, is_unpromoted: chat.is_unpromoted(), is_self_talk: chat.is_self_talk(), contacts, @@ -121,7 +121,7 @@ impl FullChat { /// - can_send /// /// used when you only need the basic metadata of a chat like type, name, profile picture -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename_all = "camelCase")] pub struct BasicChat { id: u32, @@ -155,10 +155,7 @@ impl BasicChat { is_protected: chat.is_protected(), profile_image, //BLOBS ? archived: chat.get_visibility() == chat::ChatVisibility::Archived, - chat_type: chat - .get_type() - .to_u32() - .ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap? + chat_type: chat.get_type().to_u32().context("unknown chat type id")?, is_unpromoted: chat.is_unpromoted(), is_self_talk: chat.is_self_talk(), color, @@ -169,11 +166,12 @@ impl BasicChat { } } -#[derive(Clone, Serialize, Deserialize, TypeDef)] +#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)] +#[serde(tag = "kind")] pub enum MuteDuration { NotMuted, Forever, - Until(i64), + Until { duration: i64 }, } impl MuteDuration { @@ -181,20 +179,20 @@ impl MuteDuration { match self { MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted), MuteDuration::Forever => Ok(chat::MuteDuration::Forever), - MuteDuration::Until(n) => { - if n <= 0 { + MuteDuration::Until { duration } => { + if duration <= 0 { bail!("failed to read mute duration") } Ok(SystemTime::now() - .checked_add(Duration::from_secs(n as u64)) + .checked_add(Duration::from_secs(duration as u64)) .map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until)) } } } } -#[derive(Clone, Serialize, Deserialize, TypeDef)] +#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)] #[serde(rename = "ChatVisibility")] pub enum JSONRPCChatVisibility { Normal, diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs index 83b84dd3d..b47f7a728 100644 --- a/deltachat-jsonrpc/src/api/types/chat_list.rs +++ b/deltachat-jsonrpc/src/api/types/chat_list.rs @@ -1,25 +1,21 @@ -use anyhow::Result; +use anyhow::{Context, Result}; +use deltachat::chat::{Chat, ChatId}; +use deltachat::chatlist::get_last_message_for_chat; use deltachat::constants::*; use deltachat::contact::{Contact, ContactId}; use deltachat::{ chat::{get_chat_contacts, ChatVisibility}, chatlist::Chatlist, }; -use deltachat::{ - chat::{Chat, ChatId}, - message::MsgId, -}; use num_traits::cast::ToPrimitive; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use typescript_type_def::TypeDef; use super::color_int_to_hex_string; +use super::message::MessageViewtype; -#[derive(Deserialize, Serialize, TypeDef)] -pub struct ChatListEntry(pub u32, pub u32); - -#[derive(Serialize, TypeDef)] -#[serde(tag = "type")] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] +#[serde(tag = "kind")] pub enum ChatListItemFetchResult { #[serde(rename_all = "camelCase")] ChatListItem { @@ -31,6 +27,8 @@ pub enum ChatListItemFetchResult { summary_text1: String, summary_text2: String, summary_status: u32, + /// showing preview if last chat message is image + summary_preview_image: Option, is_protected: bool, is_group: bool, fresh_message_counter: usize, @@ -47,6 +45,8 @@ pub enum ChatListItemFetchResult { /// contact id if this is a dm chat (for view profile entry in context menu) dm_chat_contact: Option, was_seen_recently: bool, + last_message_type: Option, + last_message_id: Option, }, #[serde(rename_all = "camelCase")] ArchiveLink { fresh_message_counter: usize }, @@ -56,14 +56,9 @@ pub enum ChatListItemFetchResult { pub(crate) async fn get_chat_list_item_by_id( ctx: &deltachat::context::Context, - entry: &ChatListEntry, + entry: u32, ) -> Result { - let chat_id = ChatId::new(entry.0); - let last_msgid = match entry.1 { - 0 => None, - _ => Some(MsgId::new(entry.1)), - }; - + let chat_id = ChatId::new(entry); let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?; if chat_id.is_archived_link() { @@ -72,12 +67,18 @@ pub(crate) async fn get_chat_list_item_by_id( }); } - let chat = Chat::load_from_db(ctx, chat_id).await?; - let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat)).await?; + let last_msgid = get_last_message_for_chat(ctx, chat_id).await?; + + let chat = Chat::load_from_db(ctx, chat_id).await.context("chat")?; + let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat)) + .await + .context("summary")?; let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string()); let summary_text2 = summary.text.to_owned(); + let summary_preview_image = summary.thumbnail_path; + let visibility = chat.get_visibility(); let avatar_path = chat @@ -85,12 +86,15 @@ pub(crate) async fn get_chat_list_item_by_id( .await? .map(|path| path.to_str().unwrap_or("invalid/path").to_owned()); - let last_updated = match last_msgid { + let (last_updated, message_type) = match last_msgid { Some(id) => { let last_message = deltachat::message::Message::load_from_db(ctx, id).await?; - Some(last_message.get_timestamp() * 1000) + ( + Some(last_message.get_timestamp() * 1000), + Some(last_message.get_viewtype().into()), + ) } - None => None, + None => (None, None), }; let chat_contacts = get_chat_contacts(ctx, chat_id).await?; @@ -100,8 +104,9 @@ pub(crate) async fn get_chat_list_item_by_id( let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single { let contact = chat_contacts.get(0); let was_seen_recently = match contact { - Some(contact) => Contact::load_from_db(ctx, *contact) - .await? + Some(contact) => Contact::get_by_id(ctx, *contact) + .await + .context("contact")? .was_seen_recently(), None => false, }; @@ -124,6 +129,7 @@ pub(crate) async fn get_chat_list_item_by_id( summary_text1, summary_text2, summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum + summary_preview_image, is_protected: chat.is_protected(), is_group: chat.get_type() == Chattype::Group, fresh_message_counter, @@ -138,5 +144,7 @@ pub(crate) async fn get_chat_list_item_by_id( is_broadcast: chat.get_type() == Chattype::Broadcast, dm_chat_contact, was_seen_recently, + last_message_type: message_type, + last_message_id: last_msgid.map(|id| id.to_u32()), }) } diff --git a/deltachat-jsonrpc/src/api/types/contact.rs b/deltachat-jsonrpc/src/api/types/contact.rs index d67fc7fb1..53b0ef67c 100644 --- a/deltachat-jsonrpc/src/api/types/contact.rs +++ b/deltachat-jsonrpc/src/api/types/contact.rs @@ -6,7 +6,7 @@ use typescript_type_def::TypeDef; use super::color_int_to_hex_string; -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename = "Contact", rename_all = "camelCase")] pub struct ContactObject { address: String, diff --git a/deltachat-jsonrpc/src/api/events.rs b/deltachat-jsonrpc/src/api/types/events.rs similarity index 77% rename from deltachat-jsonrpc/src/api/events.rs rename to deltachat-jsonrpc/src/api/types/events.rs index ddbbcebca..dd03358bc 100644 --- a/deltachat-jsonrpc/src/api/events.rs +++ b/deltachat-jsonrpc/src/api/types/events.rs @@ -1,19 +1,29 @@ -use deltachat::{Event, EventType}; +use deltachat::{Event as CoreEvent, EventType as CoreEventType}; use serde::Serialize; -use serde_json::{json, Value}; use typescript_type_def::TypeDef; -pub fn event_to_json_rpc_notification(event: Event) -> Value { - let id: JSONRPCEventType = event.typ.into(); - json!({ - "event": id, - "contextId": event.id, - }) +#[derive(Serialize, TypeDef, schemars::JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct Event { + /// Event payload. + event: EventType, + + /// Account ID. + context_id: u32, } -#[derive(Serialize, TypeDef)] -#[serde(tag = "type", rename = "Event")] -pub enum JSONRPCEventType { +impl From for Event { + fn from(event: CoreEvent) -> Self { + Event { + event: event.typ.into(), + context_id: event.id, + } + } +} + +#[derive(Serialize, TypeDef, schemars::JsonSchema)] +#[serde(tag = "kind")] +pub enum EventType { /// The library-user may write an informational string to the log. /// /// This event should *not* be reported to the end-user using a popup or something like @@ -164,6 +174,13 @@ pub enum JSONRPCEventType { msg_id: u32, }, + /// A single message is deleted. + #[serde(rename_all = "camelCase")] + MsgDeleted { + chat_id: u32, + msg_id: u32, + }, + /// Chat changed. The name or the image of a chat group was changed or members were added or removed. /// Or the verify state of a chat has changed. /// See setChatName(), setChatProfileImage(), addContactToChat() @@ -286,27 +303,27 @@ pub enum JSONRPCEventType { }, } -impl From for JSONRPCEventType { - fn from(event: EventType) -> Self { - use JSONRPCEventType::*; +impl From for EventType { + fn from(event: CoreEventType) -> Self { + use EventType::*; match event { - EventType::Info(msg) => Info { msg }, - EventType::SmtpConnected(msg) => SmtpConnected { msg }, - EventType::ImapConnected(msg) => ImapConnected { msg }, - EventType::SmtpMessageSent(msg) => SmtpMessageSent { msg }, - EventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg }, - EventType::ImapMessageMoved(msg) => ImapMessageMoved { msg }, - EventType::ImapInboxIdle => ImapInboxIdle, - EventType::NewBlobFile(file) => NewBlobFile { file }, - EventType::DeletedBlobFile(file) => DeletedBlobFile { file }, - EventType::Warning(msg) => Warning { msg }, - EventType::Error(msg) => Error { msg }, - EventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg }, - EventType::MsgsChanged { chat_id, msg_id } => MsgsChanged { + CoreEventType::Info(msg) => Info { msg }, + CoreEventType::SmtpConnected(msg) => SmtpConnected { msg }, + CoreEventType::ImapConnected(msg) => ImapConnected { msg }, + CoreEventType::SmtpMessageSent(msg) => SmtpMessageSent { msg }, + CoreEventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg }, + CoreEventType::ImapMessageMoved(msg) => ImapMessageMoved { msg }, + CoreEventType::ImapInboxIdle => ImapInboxIdle, + CoreEventType::NewBlobFile(file) => NewBlobFile { file }, + CoreEventType::DeletedBlobFile(file) => DeletedBlobFile { file }, + CoreEventType::Warning(msg) => Warning { msg }, + CoreEventType::Error(msg) => Error { msg }, + CoreEventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg }, + CoreEventType::MsgsChanged { chat_id, msg_id } => MsgsChanged { chat_id: chat_id.to_u32(), msg_id: msg_id.to_u32(), }, - EventType::ReactionsChanged { + CoreEventType::ReactionsChanged { chat_id, msg_id, contact_id, @@ -315,92 +332,80 @@ impl From for JSONRPCEventType { msg_id: msg_id.to_u32(), contact_id: contact_id.to_u32(), }, - EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg { + CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg { chat_id: chat_id.to_u32(), msg_id: msg_id.to_u32(), }, - EventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch { + CoreEventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch { msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(), }, - EventType::MsgsNoticed(chat_id) => MsgsNoticed { + CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed { chat_id: chat_id.to_u32(), }, - EventType::MsgDelivered { chat_id, msg_id } => MsgDelivered { + CoreEventType::MsgDelivered { chat_id, msg_id } => MsgDelivered { chat_id: chat_id.to_u32(), msg_id: msg_id.to_u32(), }, - EventType::MsgFailed { chat_id, msg_id } => MsgFailed { + CoreEventType::MsgFailed { chat_id, msg_id } => MsgFailed { chat_id: chat_id.to_u32(), msg_id: msg_id.to_u32(), }, - EventType::MsgRead { chat_id, msg_id } => MsgRead { + CoreEventType::MsgRead { chat_id, msg_id } => MsgRead { chat_id: chat_id.to_u32(), msg_id: msg_id.to_u32(), }, - EventType::ChatModified(chat_id) => ChatModified { + CoreEventType::MsgDeleted { chat_id, msg_id } => MsgDeleted { + chat_id: chat_id.to_u32(), + msg_id: msg_id.to_u32(), + }, + CoreEventType::ChatModified(chat_id) => ChatModified { chat_id: chat_id.to_u32(), }, - EventType::ChatEphemeralTimerModified { chat_id, timer } => { + CoreEventType::ChatEphemeralTimerModified { chat_id, timer } => { ChatEphemeralTimerModified { chat_id: chat_id.to_u32(), timer: timer.to_u32(), } } - EventType::ContactsChanged(contact) => ContactsChanged { + CoreEventType::ContactsChanged(contact) => ContactsChanged { contact_id: contact.map(|c| c.to_u32()), }, - EventType::LocationChanged(contact) => LocationChanged { + CoreEventType::LocationChanged(contact) => LocationChanged { contact_id: contact.map(|c| c.to_u32()), }, - EventType::ConfigureProgress { progress, comment } => { + CoreEventType::ConfigureProgress { progress, comment } => { ConfigureProgress { progress, comment } } - EventType::ImexProgress(progress) => ImexProgress { progress }, - EventType::ImexFileWritten(path) => ImexFileWritten { + CoreEventType::ImexProgress(progress) => ImexProgress { progress }, + CoreEventType::ImexFileWritten(path) => ImexFileWritten { path: path.to_str().unwrap_or_default().to_owned(), }, - EventType::SecurejoinInviterProgress { + CoreEventType::SecurejoinInviterProgress { contact_id, progress, } => SecurejoinInviterProgress { contact_id: contact_id.to_u32(), progress, }, - EventType::SecurejoinJoinerProgress { + CoreEventType::SecurejoinJoinerProgress { contact_id, progress, } => SecurejoinJoinerProgress { contact_id: contact_id.to_u32(), progress, }, - EventType::ConnectivityChanged => ConnectivityChanged, - EventType::SelfavatarChanged => SelfavatarChanged, - EventType::WebxdcStatusUpdate { + CoreEventType::ConnectivityChanged => ConnectivityChanged, + CoreEventType::SelfavatarChanged => SelfavatarChanged, + CoreEventType::WebxdcStatusUpdate { msg_id, status_update_serial, } => WebxdcStatusUpdate { msg_id: msg_id.to_u32(), status_update_serial: status_update_serial.to_u32(), }, - EventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted { + CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted { msg_id: msg_id.to_u32(), }, } } } - -#[cfg(test)] -#[test] -fn generate_events_ts_types_definition() { - let events = { - let mut buf = Vec::new(); - let options = typescript_type_def::DefinitionFileOptions { - root_namespace: None, - ..typescript_type_def::DefinitionFileOptions::default() - }; - typescript_type_def::write_definition_file::<_, JSONRPCEventType>(&mut buf, options) - .unwrap(); - String::from_utf8(buf).unwrap() - }; - std::fs::write("typescript/generated/events.ts", events).unwrap(); -} diff --git a/deltachat-jsonrpc/src/api/types/http.rs b/deltachat-jsonrpc/src/api/types/http.rs new file mode 100644 index 000000000..9121a677e --- /dev/null +++ b/deltachat-jsonrpc/src/api/types/http.rs @@ -0,0 +1,29 @@ +use deltachat::net::HttpResponse as CoreHttpResponse; +use serde::Serialize; +use typescript_type_def::TypeDef; + +#[derive(Serialize, TypeDef, schemars::JsonSchema)] +pub struct HttpResponse { + /// base64-encoded response body. + blob: String, + + /// MIME type, e.g. "text/plain" or "text/html". + mimetype: Option, + + /// Encoding, e.g. "utf-8". + encoding: Option, +} + +impl From for HttpResponse { + fn from(response: CoreHttpResponse) -> Self { + use base64::{engine::general_purpose, Engine as _}; + let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob); + let mimetype = response.mimetype; + let encoding = response.encoding; + HttpResponse { + blob, + mimetype, + encoding, + } + } +} diff --git a/deltachat-jsonrpc/src/api/types/location.rs b/deltachat-jsonrpc/src/api/types/location.rs index 374e408d8..f610b42d3 100644 --- a/deltachat-jsonrpc/src/api/types/location.rs +++ b/deltachat-jsonrpc/src/api/types/location.rs @@ -2,7 +2,7 @@ use deltachat::location::Location; use serde::Serialize; use typescript_type_def::TypeDef; -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename = "Location", rename_all = "camelCase")] pub struct JsonrpcLocation { pub location_id: u32, diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index 44d2a1f05..4bd691e3a 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -1,7 +1,7 @@ -use anyhow::{anyhow, Result}; +use anyhow::{Context as _, Result}; use deltachat::chat::Chat; use deltachat::chat::ChatItem; -use deltachat::constants::Chattype; +use deltachat::chat::ChatVisibility; use deltachat::contact::Contact; use deltachat::context::Context; use deltachat::download; @@ -10,8 +10,7 @@ use deltachat::message::MsgId; use deltachat::message::Viewtype; use deltachat::reaction::get_msg_reactions; use num_traits::cast::ToPrimitive; -use serde::Deserialize; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use typescript_type_def::TypeDef; use super::color_int_to_hex_string; @@ -19,14 +18,14 @@ use super::contact::ContactObject; use super::reactions::JSONRPCReactions; use super::webxdc::WebxdcMessageInfo; -#[derive(Serialize, TypeDef)] -#[serde(rename_all = "camelCase", tag = "variant")] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] +#[serde(rename_all = "camelCase", tag = "kind")] pub enum MessageLoadResult { Message(MessageObject), LoadingError { error: String }, } -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename = "Message", rename_all = "camelCase")] pub struct MessageObject { id: u32, @@ -35,7 +34,7 @@ pub struct MessageObject { quote: Option, parent_id: Option, - text: Option, + text: String, has_location: bool, has_html: bool, view_type: MessageViewtype, @@ -86,7 +85,7 @@ pub struct MessageObject { reactions: Option, } -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(tag = "kind")] enum MessageQuote { JustText { @@ -114,8 +113,12 @@ impl MessageObject { pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result { let message = Message::load_from_db(context, msg_id).await?; - let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?; - let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?; + let sender_contact = Contact::get_by_id(context, message.get_from_id()) + .await + .context("failed to load sender contact")?; + let sender = ContactObject::try_from_dc_contact(context, sender_contact) + .await + .context("failed to load sender contact object")?; let file_bytes = message.get_filebytes(context).await?.unwrap_or_default(); let override_sender_name = message.get_override_sender_name(); @@ -132,7 +135,9 @@ impl MessageObject { let quote = if let Some(quoted_text) = message.quoted_text() { match message.quoted_message(context).await? { Some(quote) => { - let quote_author = Contact::load_from_db(context, quote.get_from_id()).await?; + let quote_author = Contact::get_by_id(context, quote.get_from_id()) + .await + .context("failed to load quote author contact")?; Some(MessageQuote::WithMessage { text: quoted_text, message_id: quote.get_id().to_u32(), @@ -160,7 +165,9 @@ impl MessageObject { None }; - let reactions = get_msg_reactions(context, msg_id).await?; + let reactions = get_msg_reactions(context, msg_id) + .await + .context("failed to load message reactions")?; let reactions = if reactions.is_empty() { None } else { @@ -180,7 +187,7 @@ impl MessageObject { state: message .get_state() .to_u32() - .ok_or_else(|| anyhow!("state conversion to number failed"))?, + .context("state conversion to number failed")?, error: message.error(), timestamp: message.get_timestamp(), @@ -203,7 +210,7 @@ impl MessageObject { videochat_type: match message.get_videochat_type() { Some(vct) => Some( vct.to_u32() - .ok_or_else(|| anyhow!("state conversion to number failed"))?, + .context("videochat type conversion to number failed")?, ), None => None, }, @@ -230,7 +237,7 @@ impl MessageObject { } } -#[derive(Serialize, Deserialize, TypeDef)] +#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)] #[serde(rename = "Viewtype")] pub enum MessageViewtype { Unknown, @@ -306,11 +313,12 @@ impl From for Viewtype { } } -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] pub enum DownloadState { Done, Available, Failure, + Undecipherable, InProgress, } @@ -320,12 +328,13 @@ impl From for DownloadState { download::DownloadState::Done => DownloadState::Done, download::DownloadState::Available => DownloadState::Available, download::DownloadState::Failure => DownloadState::Failure, + download::DownloadState::Undecipherable => DownloadState::Undecipherable, download::DownloadState::InProgress => DownloadState::InProgress, } } } -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] pub enum SystemMessageType { Unknown, GroupNameChanged, @@ -380,7 +389,7 @@ impl From for SystemMessageType { } } -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename_all = "camelCase")] pub struct MessageNotificationInfo { id: u32, @@ -438,14 +447,22 @@ impl MessageNotificationInfo { } } -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename_all = "camelCase")] pub struct MessageSearchResult { id: u32, author_profile_image: Option, + /// if sender name if overridden it will show it as ~alias author_name: String, author_color: String, - chat_name: Option, + author_id: u32, + chat_profile_image: Option, + chat_color: String, + chat_name: String, + chat_type: u32, + is_chat_protected: bool, + is_chat_contact_request: bool, + is_chat_archived: bool, message: String, timestamp: i64, } @@ -454,30 +471,44 @@ impl MessageSearchResult { pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result { let message = Message::load_from_db(context, msg_id).await?; let chat = Chat::load_from_db(context, message.get_chat_id()).await?; - let sender = Contact::load_from_db(context, message.get_from_id()).await?; + let sender = Contact::get_by_id(context, message.get_from_id()).await?; let profile_image = match sender.get_profile_image(context).await? { Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()), None => None, }; + let chat_profile_image = match chat.get_profile_image(context).await? { + Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()), + None => None, + }; + + let author_name = if let Some(name) = message.get_override_sender_name() { + format!("~{name}") + } else { + sender.get_display_name().to_owned() + }; + let chat_color = color_int_to_hex_string(chat.get_color(context).await?); Ok(Self { id: msg_id.to_u32(), author_profile_image: profile_image, - author_name: sender.get_display_name().to_owned(), + author_name, author_color: color_int_to_hex_string(sender.get_color()), - chat_name: if chat.get_type() == Chattype::Single { - Some(chat.get_name().to_owned()) - } else { - None - }, - message: message.get_text().unwrap_or_default(), + author_id: sender.id.to_u32(), + chat_name: chat.get_name().to_owned(), + chat_color, + chat_type: chat.get_type().to_u32().context("unknown chat type id")?, + chat_profile_image, + is_chat_protected: chat.is_protected(), + is_chat_contact_request: chat.is_contact_request(), + is_chat_archived: chat.get_visibility() == ChatVisibility::Archived, + message: message.get_text(), timestamp: message.get_timestamp(), }) } } -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")] pub enum JSONRPCMessageListItem { Message { @@ -503,7 +534,7 @@ impl From for JSONRPCMessageListItem { } } -#[derive(Deserialize, TypeDef)] +#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename_all = "camelCase")] pub struct MessageData { pub text: Option, @@ -514,3 +545,10 @@ pub struct MessageData { pub override_sender_name: Option, pub quoted_message_id: Option, } + +#[derive(Serialize, TypeDef, schemars::JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct MessageReadReceipt { + pub contact_id: u32, + pub timestamp: i64, +} diff --git a/deltachat-jsonrpc/src/api/types/mod.rs b/deltachat-jsonrpc/src/api/types/mod.rs index 2d783990e..8143be73d 100644 --- a/deltachat-jsonrpc/src/api/types/mod.rs +++ b/deltachat-jsonrpc/src/api/types/mod.rs @@ -2,6 +2,8 @@ pub mod account; pub mod chat; pub mod chat_list; pub mod contact; +pub mod events; +pub mod http; pub mod location; pub mod message; pub mod provider_info; diff --git a/deltachat-jsonrpc/src/api/types/provider_info.rs b/deltachat-jsonrpc/src/api/types/provider_info.rs index 1cc5a7d46..43b868444 100644 --- a/deltachat-jsonrpc/src/api/types/provider_info.rs +++ b/deltachat-jsonrpc/src/api/types/provider_info.rs @@ -3,7 +3,7 @@ use num_traits::cast::ToPrimitive; use serde::Serialize; use typescript_type_def::TypeDef; -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename_all = "camelCase")] pub struct ProviderInfo { pub before_login_hint: String, diff --git a/deltachat-jsonrpc/src/api/types/qr.rs b/deltachat-jsonrpc/src/api/types/qr.rs index 607b495ec..0f6d79c8c 100644 --- a/deltachat-jsonrpc/src/api/types/qr.rs +++ b/deltachat-jsonrpc/src/api/types/qr.rs @@ -2,9 +2,9 @@ use deltachat::qr::Qr; use serde::Serialize; use typescript_type_def::TypeDef; -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename = "Qr", rename_all = "camelCase")] -#[serde(tag = "type")] +#[serde(tag = "kind")] pub enum QrObject { AskVerifyContact { contact_id: u32, diff --git a/deltachat-jsonrpc/src/api/types/reactions.rs b/deltachat-jsonrpc/src/api/types/reactions.rs index 8717ebdc2..37739c848 100644 --- a/deltachat-jsonrpc/src/api/types/reactions.rs +++ b/deltachat-jsonrpc/src/api/types/reactions.rs @@ -1,23 +1,37 @@ use std::collections::BTreeMap; +use deltachat::contact::ContactId; use deltachat::reaction::Reactions; use serde::Serialize; use typescript_type_def::TypeDef; +/// A single reaction emoji. +#[derive(Serialize, TypeDef, schemars::JsonSchema)] +#[serde(rename = "Reaction", rename_all = "camelCase")] +pub struct JSONRPCReaction { + /// Emoji. + emoji: String, + + /// Emoji frequency. + count: usize, + + /// True if we reacted with this emoji. + is_from_self: bool, +} + /// Structure representing all reactions to a particular message. -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename = "Reactions", rename_all = "camelCase")] pub struct JSONRPCReactions { /// Map from a contact to it's reaction to message. reactions_by_contact: BTreeMap>, - /// Unique reactions and their count - reactions: BTreeMap, + /// Unique reactions and their count, sorted in descending order. + reactions: Vec, } impl From for JSONRPCReactions { fn from(reactions: Reactions) -> Self { let mut reactions_by_contact: BTreeMap> = BTreeMap::new(); - let mut unique_reactions: BTreeMap = BTreeMap::new(); for contact_id in reactions.contacts() { let reaction = reactions.get(contact_id); @@ -30,18 +44,29 @@ impl From for JSONRPCReactions { .map(|emoji| emoji.to_owned()) .collect(); reactions_by_contact.insert(contact_id.to_u32(), emojis.clone()); - for emoji in emojis { - if let Some(x) = unique_reactions.get_mut(&emoji) { - *x += 1; - } else { - unique_reactions.insert(emoji, 1); - } - } + } + + let self_reactions = reactions_by_contact.get(&ContactId::SELF.to_u32()); + + let mut reactions_v = Vec::new(); + for (emoji, count) in reactions.emoji_sorted_by_frequency() { + let is_from_self = if let Some(self_reactions) = self_reactions { + self_reactions.contains(&emoji) + } else { + false + }; + + let reaction = JSONRPCReaction { + emoji, + count, + is_from_self, + }; + reactions_v.push(reaction) } JSONRPCReactions { reactions_by_contact, - reactions: unique_reactions, + reactions: reactions_v, } } } diff --git a/deltachat-jsonrpc/src/api/types/webxdc.rs b/deltachat-jsonrpc/src/api/types/webxdc.rs index f89310667..71db9ed93 100644 --- a/deltachat-jsonrpc/src/api/types/webxdc.rs +++ b/deltachat-jsonrpc/src/api/types/webxdc.rs @@ -8,7 +8,7 @@ use typescript_type_def::TypeDef; use super::maybe_empty_string_to_option; -#[derive(Serialize, TypeDef)] +#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename = "WebxdcMessageInfo", rename_all = "camelCase")] pub struct WebxdcMessageInfo { /// The name of the app. diff --git a/deltachat-jsonrpc/src/lib.rs b/deltachat-jsonrpc/src/lib.rs index bb4ea9cf6..10ec39ea4 100644 --- a/deltachat-jsonrpc/src/lib.rs +++ b/deltachat-jsonrpc/src/lib.rs @@ -1,5 +1,4 @@ pub mod api; -pub use api::events; pub use yerpc; #[cfg(test)] @@ -14,7 +13,8 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn basic_json_rpc_functionality() -> anyhow::Result<()> { let tmp_dir = TempDir::new().unwrap().path().into(); - let accounts = Accounts::new(tmp_dir).await?; + let writable = true; + let accounts = Accounts::new(tmp_dir, writable).await?; let api = CommandApi::new(accounts); let (sender, mut receiver) = unbounded::(); @@ -55,7 +55,8 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_batch_set_config() -> anyhow::Result<()> { let tmp_dir = TempDir::new().unwrap().path().into(); - let accounts = Accounts::new(tmp_dir).await?; + let writable = true; + let accounts = Accounts::new(tmp_dir, writable).await?; let api = CommandApi::new(accounts); let (sender, mut receiver) = unbounded::(); diff --git a/deltachat-jsonrpc/src/webserver.rs b/deltachat-jsonrpc/src/webserver.rs index 9231069c5..f4b6f38af 100644 --- a/deltachat-jsonrpc/src/webserver.rs +++ b/deltachat-jsonrpc/src/webserver.rs @@ -6,7 +6,6 @@ use yerpc::axum::handle_ws_rpc; use yerpc::{RpcClient, RpcSession}; mod api; -use api::events::event_to_json_rpc_notification; use api::{Accounts, CommandApi}; const DEFAULT_PORT: u16 = 20808; @@ -20,7 +19,8 @@ async fn main() -> Result<(), std::io::Error> { .map(|port| port.parse::().expect("DC_PORT must be a number")) .unwrap_or(DEFAULT_PORT); log::info!("Starting with accounts directory `{path}`."); - let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap(); + let writable = true; + let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap(); let state = CommandApi::new(accounts); let app = Router::new() @@ -44,12 +44,5 @@ async fn main() -> Result<(), std::io::Error> { async fn handler(ws: WebSocketUpgrade, Extension(api): Extension) -> Response { let (client, out_receiver) = RpcClient::new(); let session = RpcSession::new(client.clone(), api.clone()); - tokio::spawn(async move { - let events = api.accounts.read().await.get_event_emitter(); - while let Some(event) = events.recv().await { - let event = event_to_json_rpc_notification(event); - client.send_notification("event", Some(event)).await.ok(); - } - }); handle_ws_rpc(ws, out_receiver, session).await } diff --git a/deltachat-jsonrpc/typescript/example/example.ts b/deltachat-jsonrpc/typescript/example/example.ts index 0caa2de06..e45bc18cc 100644 --- a/deltachat-jsonrpc/typescript/example/example.ts +++ b/deltachat-jsonrpc/typescript/example/example.ts @@ -35,7 +35,7 @@ async function run() { const accounts = await client.rpc.getAllAccounts(); console.log("accounts loaded", accounts); for (const account of accounts) { - if (account.type === "Configured") { + if (account.kind === "Configured") { write( $head, ` @@ -57,7 +57,7 @@ async function run() { clear($main); const selectedAccount = SELECTED_ACCOUNT; const info = await client.rpc.getAccountInfo(selectedAccount); - if (info.type !== "Configured") { + if (info.kind !== "Configured") { return write($main, "Account is not configured"); } write($main, `

${info.addr!}

`); @@ -67,7 +67,7 @@ async function run() { null, null ); - for (const [chatId, _messageId] of chats) { + for (const chatId of chats) { const chat = await client.rpc.getFullChatById(selectedAccount, chatId); write($main, `

${chat.name}

`); const messageIds = await client.rpc.getMessageIds( @@ -81,8 +81,7 @@ async function run() { messageIds ); for (const [_messageId, message] of Object.entries(messages)) { - if (message.variant === "message") - write($main, `

${message.text}

`); + if (message.kind === "message") write($main, `

${message.text}

`); else write($main, `

loading error: ${message.error}

`); } } @@ -93,9 +92,9 @@ async function run() { $side, `

- [${event.type} on account ${accountId}]
+ [${event.kind} on account ${accountId}]
f1: ${JSON.stringify( - Object.assign({}, event, { type: undefined }) + Object.assign({}, event, { kind: undefined }) )}

` ); diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index e2743bba7..b32de8e51 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -55,5 +55,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "1.112.6" + "version": "1.126.1" } diff --git a/deltachat-jsonrpc/typescript/src/client.ts b/deltachat-jsonrpc/typescript/src/client.ts index 9efbab964..83cc2f7e7 100644 --- a/deltachat-jsonrpc/typescript/src/client.ts +++ b/deltachat-jsonrpc/typescript/src/client.ts @@ -1,34 +1,28 @@ import * as T from "../generated/types.js"; +import { EventType } from "../generated/types.js"; import * as RPC from "../generated/jsonrpc.js"; import { RawClient } from "../generated/client.js"; -import { Event } from "../generated/events.js"; import { WebsocketTransport, BaseTransport, Request } from "yerpc"; import { TinyEmitter } from "@deltachat/tiny-emitter"; -type DCWireEvent = { - event: T; - contextId: number; -}; -// export type Events = Record< -// Event["type"] | "ALL", -// (event: DeltaChatEvent) => void -// >; - -type Events = { ALL: (accountId: number, event: Event) => void } & { - [Property in Event["type"]]: ( +type Events = { ALL: (accountId: number, event: EventType) => void } & { + [Property in EventType["kind"]]: ( accountId: number, - event: Extract + event: Extract ) => void; }; -type ContextEvents = { ALL: (event: Event) => void } & { - [Property in Event["type"]]: ( - event: Extract +type ContextEvents = { ALL: (event: EventType) => void } & { + [Property in EventType["kind"]]: ( + event: Extract ) => void; }; -export type DcEvent = Event; -export type DcEventType = Extract; +export type DcEvent = EventType; +export type DcEventType = Extract< + EventType, + { kind: T } +>; export class BaseDeltaChat< Transport extends BaseTransport @@ -36,27 +30,34 @@ export class BaseDeltaChat< rpc: RawClient; account?: T.Account; private contextEmitters: { [key: number]: TinyEmitter } = {}; - constructor(public transport: Transport) { + + //@ts-ignore + private eventTask: Promise; + + constructor(public transport: Transport, startEventLoop: boolean) { super(); this.rpc = new RawClient(this.transport); - this.transport.on("request", (request: Request) => { - const method = request.method; - if (method === "event") { - const event = request.params! as DCWireEvent; - //@ts-ignore - this.emit(event.event.type, event.contextId, event.event as any); - this.emit("ALL", event.contextId, event.event as any); + if (startEventLoop) { + this.eventTask = this.eventLoop(); + } + } - if (this.contextEmitters[event.contextId]) { - this.contextEmitters[event.contextId].emit( - event.event.type, - //@ts-ignore - event.event as any - ); - this.contextEmitters[event.contextId].emit("ALL", event.event); - } + async eventLoop(): Promise { + while (true) { + const event = await this.rpc.getNextEvent(); + //@ts-ignore + this.emit(event.event.kind, event.contextId, event.event); + this.emit("ALL", event.contextId, event.event); + + if (this.contextEmitters[event.contextId]) { + this.contextEmitters[event.contextId].emit( + event.event.kind, + //@ts-ignore + event.event as any + ); + this.contextEmitters[event.contextId].emit("ALL", event.event as any); } - }); + } } async listAccounts(): Promise { @@ -75,10 +76,12 @@ export class BaseDeltaChat< export type Opts = { url: string; + startEventLoop: boolean; }; export const DEFAULT_OPTS: Opts = { url: "ws://localhost:20808/ws", + startEventLoop: true, }; export class DeltaChat extends BaseDeltaChat { opts: Opts; @@ -86,20 +89,24 @@ export class DeltaChat extends BaseDeltaChat { this.transport.close(); } constructor(opts?: Opts | string) { - if (typeof opts === "string") opts = { url: opts }; - if (opts) opts = { ...DEFAULT_OPTS, ...opts }; - else opts = { ...DEFAULT_OPTS }; + if (typeof opts === "string") { + opts = { ...DEFAULT_OPTS, url: opts }; + } else if (opts) { + opts = { ...DEFAULT_OPTS, ...opts }; + } else { + opts = { ...DEFAULT_OPTS }; + } const transport = new WebsocketTransport(opts.url); - super(transport); + super(transport, opts.startEventLoop); this.opts = opts; } } export class StdioDeltaChat extends BaseDeltaChat { close() {} - constructor(input: any, output: any) { + constructor(input: any, output: any, startEventLoop: boolean) { const transport = new StdioTransport(input, output); - super(transport); + super(transport, startEventLoop); } } diff --git a/deltachat-jsonrpc/typescript/src/lib.ts b/deltachat-jsonrpc/typescript/src/lib.ts index 473d2bd33..de357a1ea 100644 --- a/deltachat-jsonrpc/typescript/src/lib.ts +++ b/deltachat-jsonrpc/typescript/src/lib.ts @@ -1,6 +1,5 @@ export * as RPC from "../generated/jsonrpc.js"; export * as T from "../generated/types.js"; -export * from "../generated/events.js"; export { RawClient } from "../generated/client.js"; export * from "./client.js"; export * as yerpc from "yerpc"; diff --git a/deltachat-jsonrpc/typescript/test/basic.ts b/deltachat-jsonrpc/typescript/test/basic.ts index 1589a733d..1c430f3f5 100644 --- a/deltachat-jsonrpc/typescript/test/basic.ts +++ b/deltachat-jsonrpc/typescript/test/basic.ts @@ -12,7 +12,7 @@ describe("basic tests", () => { before(async () => { serverHandle = await startServer(); - dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout); + dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true); // dc.on("ALL", (event) => { //console.log("event", event); // }); diff --git a/deltachat-jsonrpc/typescript/test/online.ts b/deltachat-jsonrpc/typescript/test/online.ts index 418f14679..c85376b09 100644 --- a/deltachat-jsonrpc/typescript/test/online.ts +++ b/deltachat-jsonrpc/typescript/test/online.ts @@ -27,10 +27,10 @@ describe("online tests", function () { this.skip(); } serverHandle = await startServer(); - dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout); + dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true); - dc.on("ALL", (contextId, { type }) => { - if (type !== "Info") console.log(contextId, type); + dc.on("ALL", (contextId, { kind }) => { + if (kind !== "Info") console.log(contextId, kind); }); account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL); @@ -148,7 +148,7 @@ describe("online tests", function () { waitForEvent(dc, "IncomingMsg", accountId1), ]); dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message"); - // Check if answer arives at A and if it is encrypted + // Check if answer arrives at A and if it is encrypted await eventPromise2; const messageId = ( @@ -177,12 +177,12 @@ describe("online tests", function () { }); }); -async function waitForEvent( +async function waitForEvent( dc: DeltaChat, eventType: T, accountId: number, timeout: number = EVENT_TIMEOUT -): Promise> { +): Promise> { return new Promise((resolve, reject) => { const rejectTimeout = setTimeout( () => reject(new Error("Timeout reached before event came in")), diff --git a/deltachat-repl/Cargo.toml b/deltachat-repl/Cargo.toml index 3eca44d6a..dd5cb0a67 100644 --- a/deltachat-repl/Cargo.toml +++ b/deltachat-repl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-repl" -version = "1.112.6" +version = "1.126.1" license = "MPL-2.0" edition = "2021" @@ -9,10 +9,10 @@ ansi_term = "0.12.1" anyhow = "1" deltachat = { path = "..", features = ["internals"]} dirs = "5" -log = "0.4.16" -pretty_env_logger = "0.4" +log = "0.4.20" +pretty_env_logger = "0.5" rusqlite = "0.29" -rustyline = "11" +rustyline = "12" tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] } [features] diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index dc4f75e12..40fa149fa 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -18,6 +18,7 @@ use deltachat::imex::*; use deltachat::location; use deltachat::log::LogExt; use deltachat::message::{self, Message, MessageState, MsgId, Viewtype}; +use deltachat::mimeparser::SystemMessage; use deltachat::peerstate::*; use deltachat::qr::*; use deltachat::reaction::send_reaction; @@ -138,11 +139,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool { /* import a directory */ let dir_name = std::path::Path::new(&real_spec); let dir = fs::read_dir(dir_name).await; - if dir.is_err() { - error!(context, "Import: Cannot open directory \"{}\".", &real_spec,); - return false; - } else { - let mut dir = dir.unwrap(); + if let Ok(mut dir) = dir { while let Ok(Some(entry)) = dir.next_entry().await { let name_f = entry.file_name(); let name = name_f.to_string_lossy(); @@ -154,6 +151,9 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool { } } } + } else { + error!(context, "Import: Cannot open directory \"{}\".", &real_spec); + return false; } } println!("Import: {} items read from \"{}\".", read_cnt, &real_spec); @@ -187,6 +187,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) { DownloadState::Available => " [⬇ Download available]", DownloadState::InProgress => " [⬇ Download in progress...]️", DownloadState::Failure => " [⬇ Download failed]", + DownloadState::Undecipherable => " [⬇ Decryption failed]", }; let temp2 = timestamp_to_str(msg.get_timestamp()); @@ -199,7 +200,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) { if msg.has_location() { "📍" } else { "" }, &contact_name, contact_id, - msgtext.unwrap_or_default(), + msgtext, if msg.has_html() { "[HAS-HTML]️" } else { "" }, if msg.get_from_id() == ContactId::SELF { "" @@ -210,7 +211,17 @@ async fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) { } else { "[FRESH]" }, - if msg.is_info() { "[INFO]" } else { "" }, + if msg.is_info() { + if msg.get_info_type() == SystemMessage::ChatProtectionEnabled { + "[INFO 🛡️]" + } else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled { + "[INFO 🛡️❌]" + } else { + "[INFO]" + } + } else { + "" + }, if msg.get_viewtype() == Viewtype::VideochatInvitation { format!( "[VIDEOCHAT-INVITATION: {}, type={}]", @@ -395,8 +406,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu unpin \n\ mute []\n\ unmute \n\ - protect \n\ - unprotect \n\ delchat \n\ accept \n\ decline \n\ @@ -805,15 +814,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu } "chatinfo" => { ensure!(sel_chat.is_some(), "No chat selected."); + let sel_chat_id = sel_chat.as_ref().unwrap().get_id(); - let contacts = - chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?; + let contacts = chat::get_chat_contacts(&context, sel_chat_id).await?; println!("Memberlist:"); log_contactlist(&context, &contacts).await?; + println!("{} contacts", contacts.len()); + + let similar_chats = sel_chat_id.get_similar_chat_ids(&context).await?; + if !similar_chats.is_empty() { + println!("Similar chats: "); + for (similar_chat_id, metric) in similar_chats { + let similar_chat = Chat::load_from_db(&context, similar_chat_id).await?; + println!( + "{} (#{}) {:.1}", + similar_chat.name, + similar_chat_id, + 100.0 * metric + ); + } + } + println!( - "{} contacts\nLocation streaming: {}", - contacts.len(), + "Location streaming: {}", location::is_sending_locations_to_chat( &context, Some(sel_chat.as_ref().unwrap().get_id()) @@ -878,7 +902,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu let latitude = arg1.parse()?; let longitude = arg2.parse()?; - let continue_streaming = location::set(&context, latitude, longitude, 0.).await; + let continue_streaming = location::set(&context, latitude, longitude, 0.).await?; if continue_streaming { println!("Success, streaming should be continued."); } else { @@ -912,9 +936,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu Viewtype::File }); msg.set_file(arg1, None); - if !arg2.is_empty() { - msg.set_text(Some(arg2.to_string())); - } + msg.set_text(arg2.to_string()); chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?; } "sendhtml" => { @@ -926,11 +948,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu let mut msg = Message::new(Viewtype::Text); msg.set_html(Some(html.to_string())); - msg.set_text(Some(if arg2.is_empty() { + msg.set_text(if arg2.is_empty() { path.file_name().unwrap().to_string_lossy().to_string() } else { arg2.to_string() - })); + }); chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?; } "sendsyncmsg" => match context.send_sync_msg().await? { @@ -979,7 +1001,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu if !arg1.is_empty() { let mut draft = Message::new(Viewtype::Text); - draft.set_text(Some(arg1.to_string())); + draft.set_text(arg1.to_string()); sel_chat .as_ref() .unwrap() @@ -1003,7 +1025,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu "Please specify text to add as device message." ); let mut msg = Message::new(Viewtype::Text); - msg.set_text(Some(arg1.to_string())); + msg.set_text(arg1.to_string()); chat::add_device_msg(&context, None, Some(&mut msg)).await?; } "listmedia" => { @@ -1058,20 +1080,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu }; chat::set_muted(&context, chat_id, duration).await?; } - "protect" | "unprotect" => { - ensure!(!arg1.is_empty(), "Argument missing."); - let chat_id = ChatId::new(arg1.parse()?); - chat_id - .set_protection( - &context, - match arg0 { - "protect" => ProtectionStatus::Protected, - "unprotect" => ProtectionStatus::Unprotected, - _ => unreachable!("arg0={:?}", arg0), - }, - ) - .await?; - } "delchat" => { ensure!(!arg1.is_empty(), "Argument missing."); let chat_id = ChatId::new(arg1.parse()?); @@ -1090,7 +1098,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu "msginfo" => { ensure!(!arg1.is_empty(), "Argument missing."); let id = MsgId::new(arg1.parse()?); - let res = message::get_msg_info(&context, id).await?; + let res = id.get_info(&context).await?; println!("{res}"); } "download" => { diff --git a/deltachat-rpc-client/LICENSE b/deltachat-rpc-client/LICENSE new file mode 100644 index 000000000..d0a1fa148 --- /dev/null +++ b/deltachat-rpc-client/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/deltachat-rpc-client/README.md b/deltachat-rpc-client/README.md index 4ce7aa7c4..4b7503fce 100644 --- a/deltachat-rpc-client/README.md +++ b/deltachat-rpc-client/README.md @@ -5,9 +5,23 @@ and provides asynchronous interface to it. ## Getting started -To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`. +To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server` +or download a prebuilt release. Install it anywhere in your `PATH`. +[Create a virtual environment](https://docs.python.org/3/library/venv.html) +if you don't have one already and activate it. +``` +$ python -m venv env +$ . env/bin/activate +``` + +Install `deltachat-rpc-client` from source: +``` +$ cd deltachat-rpc-client +$ pip install . +``` + ## Testing 1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`. @@ -23,19 +37,14 @@ $ tox --devenv env $ . env/bin/activate ``` -It is recommended to use IPython, because it supports using `await` directly -from the REPL. - ``` -$ pip install ipython -$ PATH="../target/debug:$PATH" ipython -... -In [1]: from deltachat_rpc_client import * -In [2]: rpc = Rpc() -In [3]: await rpc.start() -In [4]: dc = DeltaChat(rpc) -In [5]: system_info = await dc.get_system_info() -In [6]: system_info["level"] -Out[6]: 'awesome' -In [7]: await rpc.close() +$ python +>>> from deltachat_rpc_client import * +>>> rpc = Rpc() +>>> rpc.start() +>>> dc = DeltaChat(rpc) +>>> system_info = dc.get_system_info() +>>> system_info["level"] +'awesome' +>>> rpc.close() ``` diff --git a/deltachat-rpc-client/examples/echobot.py b/deltachat-rpc-client/examples/echobot.py index 65d447bf9..7d2d79e8b 100755 --- a/deltachat-rpc-client/examples/echobot.py +++ b/deltachat-rpc-client/examples/echobot.py @@ -4,23 +4,21 @@ it will echo back any text send to it, it also will print to console all Delta Chat core events. Pass --help to the CLI to see available options. """ -import asyncio - from deltachat_rpc_client import events, run_bot_cli hooks = events.HookCollection() @hooks.on(events.RawEvent) -async def log_event(event): +def log_event(event): print(event) @hooks.on(events.NewMessage) -async def echo(event): +def echo(event): snapshot = event.message_snapshot - await snapshot.chat.send_text(snapshot.text) + snapshot.chat.send_text(snapshot.text) if __name__ == "__main__": - asyncio.run(run_bot_cli(hooks)) + run_bot_cli(hooks) diff --git a/deltachat-rpc-client/examples/echobot_advanced.py b/deltachat-rpc-client/examples/echobot_advanced.py index 3030941c8..221f69dda 100644 --- a/deltachat-rpc-client/examples/echobot_advanced.py +++ b/deltachat-rpc-client/examples/echobot_advanced.py @@ -3,9 +3,9 @@ it will echo back any message that has non-empty text and also supports the /help command. """ -import asyncio import logging import sys +from threading import Thread from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events @@ -13,62 +13,62 @@ hooks = events.HookCollection() @hooks.on(events.RawEvent) -async def log_event(event): - if event.type == EventType.INFO: +def log_event(event): + if event.kind == EventType.INFO: logging.info(event.msg) - elif event.type == EventType.WARNING: + elif event.kind == EventType.WARNING: logging.warning(event.msg) @hooks.on(events.RawEvent(EventType.ERROR)) -async def log_error(event): +def log_error(event): logging.error(event.msg) @hooks.on(events.MemberListChanged) -async def on_memberlist_changed(event): +def on_memberlist_changed(event): logging.info("member %s was %s", event.member, "added" if event.member_added else "removed") @hooks.on(events.GroupImageChanged) -async def on_group_image_changed(event): +def on_group_image_changed(event): logging.info("group image %s", "deleted" if event.image_deleted else "changed") @hooks.on(events.GroupNameChanged) -async def on_group_name_changed(event): +def on_group_name_changed(event): logging.info("group name changed, old name: %s", event.old_name) @hooks.on(events.NewMessage(func=lambda e: not e.command)) -async def echo(event): +def echo(event): snapshot = event.message_snapshot if snapshot.text or snapshot.file: - await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file) + snapshot.chat.send_message(text=snapshot.text, file=snapshot.file) @hooks.on(events.NewMessage(command="/help")) -async def help_command(event): +def help_command(event): snapshot = event.message_snapshot - await snapshot.chat.send_text("Send me any message and I will echo it back") + snapshot.chat.send_text("Send me any message and I will echo it back") -async def main(): - async with Rpc() as rpc: +def main(): + with Rpc() as rpc: deltachat = DeltaChat(rpc) - system_info = await deltachat.get_system_info() + system_info = deltachat.get_system_info() logging.info("Running deltachat core %s", system_info.deltachat_core_version) - accounts = await deltachat.get_all_accounts() - account = accounts[0] if accounts else await deltachat.add_account() + accounts = deltachat.get_all_accounts() + account = accounts[0] if accounts else deltachat.add_account() bot = Bot(account, hooks) - if not await bot.is_configured(): - # Save a reference to avoid garbage collection of the task. - _configure_task = asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2])) - await bot.run_forever() + if not bot.is_configured(): + configure_thread = Thread(run=bot.configure, kwargs={"email": sys.argv[1], "password": sys.argv[2]}) + configure_thread.start() + bot.run_forever() if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - asyncio.run(main()) + main() diff --git a/deltachat-rpc-client/examples/echobot_no_hooks.py b/deltachat-rpc-client/examples/echobot_no_hooks.py index 77fda86e7..73e875b0d 100644 --- a/deltachat-rpc-client/examples/echobot_no_hooks.py +++ b/deltachat-rpc-client/examples/echobot_no_hooks.py @@ -2,45 +2,44 @@ """ Example echo bot without using hooks """ -import asyncio import logging import sys from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId -async def main(): - async with Rpc() as rpc: +def main(): + with Rpc() as rpc: deltachat = DeltaChat(rpc) - system_info = await deltachat.get_system_info() + system_info = deltachat.get_system_info() logging.info("Running deltachat core %s", system_info["deltachat_core_version"]) - accounts = await deltachat.get_all_accounts() - account = accounts[0] if accounts else await deltachat.add_account() + accounts = deltachat.get_all_accounts() + account = accounts[0] if accounts else deltachat.add_account() - await account.set_config("bot", "1") - if not await account.is_configured(): + account.set_config("bot", "1") + if not account.is_configured(): logging.info("Account is not configured, configuring") - await account.set_config("addr", sys.argv[1]) - await account.set_config("mail_pw", sys.argv[2]) - await account.configure() + account.set_config("addr", sys.argv[1]) + account.set_config("mail_pw", sys.argv[2]) + account.configure() logging.info("Configured") else: logging.info("Account is already configured") - await deltachat.start_io() + deltachat.start_io() - async def process_messages(): - for message in await account.get_next_messages(): - snapshot = await message.get_snapshot() + def process_messages(): + for message in account.get_next_messages(): + snapshot = message.get_snapshot() if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info: - await snapshot.chat.send_text(snapshot.text) - await snapshot.message.mark_seen() + snapshot.chat.send_text(snapshot.text) + snapshot.message.mark_seen() # Process old messages. - await process_messages() + process_messages() while True: - event = await account.wait_for_event() + event = account.wait_for_event() if event["type"] == EventType.INFO: logging.info("%s", event["msg"]) elif event["type"] == EventType.WARNING: @@ -49,9 +48,9 @@ async def main(): logging.error("%s", event["msg"]) elif event["type"] == EventType.INCOMING_MSG: logging.info("Got an incoming message") - await process_messages() + process_messages() if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - asyncio.run(main()) + main() diff --git a/deltachat-rpc-client/pyproject.toml b/deltachat-rpc-client/pyproject.toml index b0d671df8..d0253741b 100644 --- a/deltachat-rpc-client/pyproject.toml +++ b/deltachat-rpc-client/pyproject.toml @@ -5,9 +5,19 @@ build-backend = "setuptools.build_meta" [project] name = "deltachat-rpc-client" description = "Python client for Delta Chat core JSON-RPC interface" -dependencies = [ - "aiohttp", - "aiodns" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Communications :: Chat", + "Topic :: Communications :: Email" ] dynamic = [ "version" @@ -61,3 +71,6 @@ line-length = 120 [tool.isort] profile = "black" + +[tool.pytest.ini_options] +log_cli = true diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py index 727c51c80..6589813ae 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py @@ -1,4 +1,4 @@ -"""Delta Chat asynchronous high-level API""" +"""Delta Chat JSON-RPC high-level API""" from ._utils import AttrDict, run_bot_cli, run_client_cli from .account import Account from .chat import Chat diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py b/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py index ec99f6dca..90a8b3d51 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py @@ -1,7 +1,7 @@ import argparse -import asyncio import re import sys +from threading import Thread from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union if TYPE_CHECKING: @@ -43,7 +43,7 @@ class AttrDict(dict): super().__setattr__(attr, val) -async def run_client_cli( +def run_client_cli( hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None, argv: Optional[list] = None, **kwargs, @@ -54,10 +54,10 @@ async def run_client_cli( """ from .client import Client - await _run_cli(Client, hooks, argv, **kwargs) + _run_cli(Client, hooks, argv, **kwargs) -async def run_bot_cli( +def run_bot_cli( hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None, argv: Optional[list] = None, **kwargs, @@ -68,10 +68,10 @@ async def run_bot_cli( """ from .client import Bot - await _run_cli(Bot, hooks, argv, **kwargs) + _run_cli(Bot, hooks, argv, **kwargs) -async def _run_cli( +def _run_cli( client_type: Type["Client"], hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None, argv: Optional[list] = None, @@ -93,20 +93,20 @@ async def _run_cli( parser.add_argument("--password", action="store", help="password") args = parser.parse_args(argv[1:]) - async with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc: + with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc: deltachat = DeltaChat(rpc) - core_version = (await deltachat.get_system_info()).deltachat_core_version - accounts = await deltachat.get_all_accounts() - account = accounts[0] if accounts else await deltachat.add_account() + core_version = (deltachat.get_system_info()).deltachat_core_version + accounts = deltachat.get_all_accounts() + account = accounts[0] if accounts else deltachat.add_account() client = client_type(account, hooks) client.logger.debug("Running deltachat core %s", core_version) - if not await client.is_configured(): + if not client.is_configured(): assert args.email, "Account is not configured and email must be provided" assert args.password, "Account is not configured and password must be provided" - # Save a reference to avoid garbage collection of the task. - _configure_task = asyncio.create_task(client.configure(email=args.email, password=args.password)) - await client.run_forever() + configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password}) + configure_thread.start() + client.run_forever() def extract_addr(text: str) -> str: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index 4c44079f7..0c6c53a2a 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -24,63 +24,63 @@ class Account: def _rpc(self) -> "Rpc": return self.manager.rpc - async def wait_for_event(self) -> AttrDict: + def wait_for_event(self) -> AttrDict: """Wait until the next event and return it.""" - return AttrDict(await self._rpc.wait_for_event(self.id)) + return AttrDict(self._rpc.wait_for_event(self.id)) - async def remove(self) -> None: + def remove(self) -> None: """Remove the account.""" - await self._rpc.remove_account(self.id) + self._rpc.remove_account(self.id) - async def start_io(self) -> None: + def start_io(self) -> None: """Start the account I/O.""" - await self._rpc.start_io(self.id) + self._rpc.start_io(self.id) - async def stop_io(self) -> None: + def stop_io(self) -> None: """Stop the account I/O.""" - await self._rpc.stop_io(self.id) + self._rpc.stop_io(self.id) - async def get_info(self) -> AttrDict: + def get_info(self) -> AttrDict: """Return dictionary of this account configuration parameters.""" - return AttrDict(await self._rpc.get_info(self.id)) + return AttrDict(self._rpc.get_info(self.id)) - async def get_size(self) -> int: + def get_size(self) -> int: """Get the combined filesize of an account in bytes.""" - return await self._rpc.get_account_file_size(self.id) + return self._rpc.get_account_file_size(self.id) - async def is_configured(self) -> bool: + def is_configured(self) -> bool: """Return True if this account is configured.""" - return await self._rpc.is_configured(self.id) + return self._rpc.is_configured(self.id) - async def set_config(self, key: str, value: Optional[str] = None) -> None: + def set_config(self, key: str, value: Optional[str] = None) -> None: """Set configuration value.""" - await self._rpc.set_config(self.id, key, value) + self._rpc.set_config(self.id, key, value) - async def get_config(self, key: str) -> Optional[str]: + def get_config(self, key: str) -> Optional[str]: """Get configuration value.""" - return await self._rpc.get_config(self.id, key) + return self._rpc.get_config(self.id, key) - async def update_config(self, **kwargs) -> None: + def update_config(self, **kwargs) -> None: """update config values.""" for key, value in kwargs.items(): - await self.set_config(key, value) + self.set_config(key, value) - async def set_avatar(self, img_path: Optional[str] = None) -> None: + def set_avatar(self, img_path: Optional[str] = None) -> None: """Set self avatar. Passing None will discard the currently set avatar. """ - await self.set_config("selfavatar", img_path) + self.set_config("selfavatar", img_path) - async def get_avatar(self) -> Optional[str]: + def get_avatar(self) -> Optional[str]: """Get self avatar.""" - return await self.get_config("selfavatar") + return self.get_config("selfavatar") - async def configure(self) -> None: + def configure(self) -> None: """Configure an account.""" - await self._rpc.configure(self.id) + self._rpc.configure(self.id) - async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact: + def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact: """Create a new Contact or return an existing one. Calling this method will always result in the same @@ -94,24 +94,24 @@ class Account: if isinstance(obj, int): obj = Contact(self, obj) if isinstance(obj, Contact): - obj = (await obj.get_snapshot()).address - return Contact(self, await self._rpc.create_contact(self.id, obj, name)) + obj = obj.get_snapshot().address + return Contact(self, self._rpc.create_contact(self.id, obj, name)) def get_contact_by_id(self, contact_id: int) -> Contact: """Return Contact instance for the given contact ID.""" return Contact(self, contact_id) - async def get_contact_by_addr(self, address: str) -> Optional[Contact]: + def get_contact_by_addr(self, address: str) -> Optional[Contact]: """Check if an e-mail address belongs to a known and unblocked contact.""" - contact_id = await self._rpc.lookup_contact_id_by_addr(self.id, address) + contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address) return contact_id and Contact(self, contact_id) - async def get_blocked_contacts(self) -> List[AttrDict]: + def get_blocked_contacts(self) -> List[AttrDict]: """Return a list with snapshots of all blocked contacts.""" - contacts = await self._rpc.get_blocked_contacts(self.id) + contacts = self._rpc.get_blocked_contacts(self.id) return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts] - async def get_contacts( + def get_contacts( self, query: Optional[str] = None, with_self: bool = False, @@ -133,9 +133,9 @@ class Account: flags |= ContactFlag.ADD_SELF if snapshot: - contacts = await self._rpc.get_contacts(self.id, flags, query) + contacts = self._rpc.get_contacts(self.id, flags, query) return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts] - contacts = await self._rpc.get_contact_ids(self.id, flags, query) + contacts = self._rpc.get_contact_ids(self.id, flags, query) return [Contact(self, contact_id) for contact_id in contacts] @property @@ -143,7 +143,7 @@ class Account: """This account's identity as a Contact.""" return Contact(self, SpecialContactId.SELF) - async def get_chatlist( + def get_chatlist( self, query: Optional[str] = None, contact: Optional[Contact] = None, @@ -175,29 +175,29 @@ class Account: if alldone_hint: flags |= ChatlistFlag.ADD_ALLDONE_HINT - entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id) + entries = self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id) if not snapshot: - return [Chat(self, entry[0]) for entry in entries] + return [Chat(self, entry) for entry in entries] - items = await self._rpc.get_chatlist_items_by_entries(self.id, entries) + items = self._rpc.get_chatlist_items_by_entries(self.id, entries) chats = [] for item in items.values(): item["chat"] = Chat(self, item["id"]) chats.append(AttrDict(item)) return chats - async def create_group(self, name: str, protect: bool = False) -> Chat: + def create_group(self, name: str, protect: bool = False) -> Chat: """Create a new group chat. After creation, the group has only self-contact as member and is in unpromoted state. """ - return Chat(self, await self._rpc.create_group_chat(self.id, name, protect)) + return Chat(self, self._rpc.create_group_chat(self.id, name, protect)) def get_chat_by_id(self, chat_id: int) -> Chat: """Return the Chat instance with the given ID.""" return Chat(self, chat_id) - async def secure_join(self, qrdata: str) -> Chat: + def secure_join(self, qrdata: str) -> Chat: """Continue a Setup-Contact or Verified-Group-Invite protocol started on another device. @@ -208,54 +208,62 @@ class Account: :param qrdata: The text of the scanned QR code. """ - return Chat(self, await self._rpc.secure_join(self.id, qrdata)) + return Chat(self, self._rpc.secure_join(self.id, qrdata)) - async def get_qr_code(self) -> Tuple[str, str]: + def get_qr_code(self) -> Tuple[str, str]: """Get Setup-Contact QR Code text and SVG data. this data needs to be transferred to another Delta Chat account in a second channel, typically used by mobiles with QRcode-show + scan UX. """ - return await self._rpc.get_chat_securejoin_qr_code_svg(self.id, None) + return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None) def get_message_by_id(self, msg_id: int) -> Message: """Return the Message instance with the given ID.""" return Message(self, msg_id) - async def mark_seen_messages(self, messages: List[Message]) -> None: + def mark_seen_messages(self, messages: List[Message]) -> None: """Mark the given set of messages as seen.""" - await self._rpc.markseen_msgs(self.id, [msg.id for msg in messages]) + self._rpc.markseen_msgs(self.id, [msg.id for msg in messages]) - async def delete_messages(self, messages: List[Message]) -> None: + def delete_messages(self, messages: List[Message]) -> None: """Delete messages (local and remote).""" - await self._rpc.delete_messages(self.id, [msg.id for msg in messages]) + self._rpc.delete_messages(self.id, [msg.id for msg in messages]) - async def get_fresh_messages(self) -> List[Message]: + def get_fresh_messages(self) -> List[Message]: """Return the list of fresh messages, newest messages first. This call is intended for displaying notifications. If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead, to process oldest messages first. """ - fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id) + fresh_msg_ids = self._rpc.get_fresh_msgs(self.id) return [Message(self, msg_id) for msg_id in fresh_msg_ids] - async def get_next_messages(self) -> List[Message]: + def get_next_messages(self) -> List[Message]: """Return a list of next messages.""" - next_msg_ids = await self._rpc.get_next_msgs(self.id) + next_msg_ids = self._rpc.get_next_msgs(self.id) return [Message(self, msg_id) for msg_id in next_msg_ids] - async def wait_next_messages(self) -> List[Message]: + def wait_next_messages(self) -> List[Message]: """Wait for new messages and return a list of them.""" - next_msg_ids = await self._rpc.wait_next_msgs(self.id) + next_msg_ids = self._rpc.wait_next_msgs(self.id) return [Message(self, msg_id) for msg_id in next_msg_ids] - async def get_fresh_messages_in_arrival_order(self) -> List[Message]: + def get_fresh_messages_in_arrival_order(self) -> List[Message]: """Return fresh messages list sorted in the order of their arrival, with ascending IDs.""" warn( "get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.", DeprecationWarning, stacklevel=2, ) - fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id)) + fresh_msg_ids = sorted(self._rpc.get_fresh_msgs(self.id)) return [Message(self, msg_id) for msg_id in fresh_msg_ids] + + def export_backup(self, path, passphrase: str = "") -> None: + """Export backup.""" + self._rpc.export_backup(self.id, str(path), passphrase) + + def import_backup(self, path, passphrase: str = "") -> None: + """Import backup.""" + self._rpc.import_backup(self.id, str(path), passphrase) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py index a9cfad8ab..20fc11b36 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py @@ -25,7 +25,7 @@ class Chat: def _rpc(self) -> "Rpc": return self.account._rpc - async def delete(self) -> None: + def delete(self) -> None: """Delete this chat and all its messages. Note: @@ -33,83 +33,83 @@ class Chat: - does not delete messages on server - the chat or contact is not blocked, new message will arrive """ - await self._rpc.delete_chat(self.account.id, self.id) + self._rpc.delete_chat(self.account.id, self.id) - async def block(self) -> None: + def block(self) -> None: """Block this chat.""" - await self._rpc.block_chat(self.account.id, self.id) + self._rpc.block_chat(self.account.id, self.id) - async def accept(self) -> None: + def accept(self) -> None: """Accept this contact request chat.""" - await self._rpc.accept_chat(self.account.id, self.id) + self._rpc.accept_chat(self.account.id, self.id) - async def leave(self) -> None: + def leave(self) -> None: """Leave this chat.""" - await self._rpc.leave_group(self.account.id, self.id) + self._rpc.leave_group(self.account.id, self.id) - async def mute(self, duration: Optional[int] = None) -> None: + def mute(self, duration: Optional[int] = None) -> None: """Mute this chat, if a duration is not provided the chat is muted forever. :param duration: mute duration from now in seconds. Must be greater than zero. """ if duration is not None: assert duration > 0, "Invalid duration" - dur: Union[str, dict] = {"Until": duration} + dur: dict = {"kind": "Until", "duration": duration} else: - dur = "Forever" - await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur) + dur = {"kind": "Forever"} + self._rpc.set_chat_mute_duration(self.account.id, self.id, dur) - async def unmute(self) -> None: + def unmute(self) -> None: """Unmute this chat.""" - await self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted") + self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"}) - async def pin(self) -> None: + def pin(self) -> None: """Pin this chat.""" - await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED) + self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED) - async def unpin(self) -> None: + def unpin(self) -> None: """Unpin this chat.""" - await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL) + self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL) - async def archive(self) -> None: + def archive(self) -> None: """Archive this chat.""" - await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED) + self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED) - async def unarchive(self) -> None: + def unarchive(self) -> None: """Unarchive this chat.""" - await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL) + self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL) - async def set_name(self, name: str) -> None: + def set_name(self, name: str) -> None: """Set name of this chat.""" - await self._rpc.set_chat_name(self.account.id, self.id, name) + self._rpc.set_chat_name(self.account.id, self.id, name) - async def set_ephemeral_timer(self, timer: int) -> None: + def set_ephemeral_timer(self, timer: int) -> None: """Set ephemeral timer of this chat.""" - await self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer) + self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer) - async def get_encryption_info(self) -> str: + def get_encryption_info(self) -> str: """Return encryption info for this chat.""" - return await self._rpc.get_chat_encryption_info(self.account.id, self.id) + return self._rpc.get_chat_encryption_info(self.account.id, self.id) - async def get_qr_code(self) -> Tuple[str, str]: + def get_qr_code(self) -> Tuple[str, str]: """Get Join-Group QR code text and SVG data.""" - return await self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id) + return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id) - async def get_basic_snapshot(self) -> AttrDict: + def get_basic_snapshot(self) -> AttrDict: """Get a chat snapshot with basic info about this chat.""" - info = await self._rpc.get_basic_chat_info(self.account.id, self.id) + info = self._rpc.get_basic_chat_info(self.account.id, self.id) return AttrDict(chat=self, **info) - async def get_full_snapshot(self) -> AttrDict: + def get_full_snapshot(self) -> AttrDict: """Get a full snapshot of this chat.""" - info = await self._rpc.get_full_chat_by_id(self.account.id, self.id) + info = self._rpc.get_full_chat_by_id(self.account.id, self.id) return AttrDict(chat=self, **info) - async def can_send(self) -> bool: + def can_send(self) -> bool: """Return true if messages can be sent to the chat.""" - return await self._rpc.can_send(self.account.id, self.id) + return self._rpc.can_send(self.account.id, self.id) - async def send_message( + def send_message( self, text: Optional[str] = None, html: Optional[str] = None, @@ -132,30 +132,30 @@ class Chat: "overrideSenderName": override_sender_name, "quotedMessageId": quoted_msg, } - msg_id = await self._rpc.send_msg(self.account.id, self.id, draft) + msg_id = self._rpc.send_msg(self.account.id, self.id, draft) return Message(self.account, msg_id) - async def send_text(self, text: str) -> Message: + def send_text(self, text: str) -> Message: """Send a text message and return the resulting Message instance.""" - msg_id = await self._rpc.misc_send_text_message(self.account.id, self.id, text) + msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text) return Message(self.account, msg_id) - async def send_videochat_invitation(self) -> Message: + def send_videochat_invitation(self) -> Message: """Send a videochat invitation and return the resulting Message instance.""" - msg_id = await self._rpc.send_videochat_invitation(self.account.id, self.id) + msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id) return Message(self.account, msg_id) - async def send_sticker(self, path: str) -> Message: + def send_sticker(self, path: str) -> Message: """Send an sticker and return the resulting Message instance.""" - msg_id = await self._rpc.send_sticker(self.account.id, self.id, path) + msg_id = self._rpc.send_sticker(self.account.id, self.id, path) return Message(self.account, msg_id) - async def forward_messages(self, messages: List[Message]) -> None: + def forward_messages(self, messages: List[Message]) -> None: """Forward a list of messages to this chat.""" msg_ids = [msg.id for msg in messages] - await self._rpc.forward_messages(self.account.id, msg_ids, self.id) + self._rpc.forward_messages(self.account.id, msg_ids, self.id) - async def set_draft( + def set_draft( self, text: Optional[str] = None, file: Optional[str] = None, @@ -164,15 +164,15 @@ class Chat: """Set draft message.""" if isinstance(quoted_msg, Message): quoted_msg = quoted_msg.id - await self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg) + self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg) - async def remove_draft(self) -> None: + def remove_draft(self) -> None: """Remove draft message.""" - await self._rpc.remove_draft(self.account.id, self.id) + self._rpc.remove_draft(self.account.id, self.id) - async def get_draft(self) -> Optional[AttrDict]: + def get_draft(self) -> Optional[AttrDict]: """Get draft message.""" - snapshot = await self._rpc.get_draft(self.account.id, self.id) + snapshot = self._rpc.get_draft(self.account.id, self.id) if not snapshot: return None snapshot = AttrDict(snapshot) @@ -181,61 +181,61 @@ class Chat: snapshot["message"] = Message(self.account, snapshot.id) return snapshot - async def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]: + def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]: """get the list of messages in this chat.""" - msgs = await self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker) + msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker) return [Message(self.account, msg_id) for msg_id in msgs] - async def get_fresh_message_count(self) -> int: + def get_fresh_message_count(self) -> int: """Get number of fresh messages in this chat""" - return await self._rpc.get_fresh_msg_cnt(self.account.id, self.id) + return self._rpc.get_fresh_msg_cnt(self.account.id, self.id) - async def mark_noticed(self) -> None: + def mark_noticed(self) -> None: """Mark all messages in this chat as noticed.""" - await self._rpc.marknoticed_chat(self.account.id, self.id) + self._rpc.marknoticed_chat(self.account.id, self.id) - async def add_contact(self, *contact: Union[int, str, Contact]) -> None: + def add_contact(self, *contact: Union[int, str, Contact]) -> None: """Add contacts to this group.""" for cnt in contact: if isinstance(cnt, str): - contact_id = (await self.account.create_contact(cnt)).id + contact_id = self.account.create_contact(cnt).id elif not isinstance(cnt, int): contact_id = cnt.id else: contact_id = cnt - await self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id) + self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id) - async def remove_contact(self, *contact: Union[int, str, Contact]) -> None: + def remove_contact(self, *contact: Union[int, str, Contact]) -> None: """Remove members from this group.""" for cnt in contact: if isinstance(cnt, str): - contact_id = (await self.account.create_contact(cnt)).id + contact_id = self.account.create_contact(cnt).id elif not isinstance(cnt, int): contact_id = cnt.id else: contact_id = cnt - await self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id) + self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id) - async def get_contacts(self) -> List[Contact]: + def get_contacts(self) -> List[Contact]: """Get the contacts belonging to this chat. For single/direct chats self-address is not included. """ - contacts = await self._rpc.get_chat_contacts(self.account.id, self.id) + contacts = self._rpc.get_chat_contacts(self.account.id, self.id) return [Contact(self.account, contact_id) for contact_id in contacts] - async def set_image(self, path: str) -> None: + def set_image(self, path: str) -> None: """Set profile image of this chat. :param path: Full path of the image to use as the group image. """ - await self._rpc.set_chat_profile_image(self.account.id, self.id, path) + self._rpc.set_chat_profile_image(self.account.id, self.id, path) - async def remove_image(self) -> None: + def remove_image(self) -> None: """Remove profile image of this chat.""" - await self._rpc.set_chat_profile_image(self.account.id, self.id, None) + self._rpc.set_chat_profile_image(self.account.id, self.id, None) - async def get_locations( + def get_locations( self, contact: Optional[Contact] = None, timestamp_from: Optional["datetime"] = None, @@ -246,7 +246,7 @@ class Chat: time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0 contact_id = contact.id if contact else 0 - result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to) + result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to) locations = [] contacts: Dict[int, Contact] = {} for loc in result: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/client.py b/deltachat-rpc-client/src/deltachat_rpc_client/client.py index 5205a1ed9..31b628ffa 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/client.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/client.py @@ -1,10 +1,8 @@ """Event loop implementations offering high level event handling/hooking.""" -import inspect import logging from typing import ( TYPE_CHECKING, Callable, - Coroutine, Dict, Iterable, Optional, @@ -78,22 +76,22 @@ class Client: ) self._hooks.get(type(event), set()).remove((hook, event)) - async def is_configured(self) -> bool: - return await self.account.is_configured() + def is_configured(self) -> bool: + return self.account.is_configured() - async def configure(self, email: str, password: str, **kwargs) -> None: - await self.account.set_config("addr", email) - await self.account.set_config("mail_pw", password) + def configure(self, email: str, password: str, **kwargs) -> None: + self.account.set_config("addr", email) + self.account.set_config("mail_pw", password) for key, value in kwargs.items(): - await self.account.set_config(key, value) - await self.account.configure() + self.account.set_config(key, value) + self.account.configure() self.logger.debug("Account configured") - async def run_forever(self) -> None: + def run_forever(self) -> None: """Process events forever.""" - await self.run_until(lambda _: False) + self.run_until(lambda _: False) - async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict: + def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict: """Process events until the given callable evaluates to True. The callable should accept an AttrDict object representing the @@ -101,39 +99,37 @@ class Client: evaluates to True. """ self.logger.debug("Listening to incoming events...") - if await self.is_configured(): - await self.account.start_io() - await self._process_messages() # Process old messages. + if self.is_configured(): + self.account.start_io() + self._process_messages() # Process old messages. while True: - event = await self.account.wait_for_event() - event["type"] = EventType(event.type) + event = self.account.wait_for_event() + event["kind"] = EventType(event.kind) event["account"] = self.account - await self._on_event(event) - if event.type == EventType.INCOMING_MSG: - await self._process_messages() + self._on_event(event) + if event.kind == EventType.INCOMING_MSG: + self._process_messages() stop = func(event) - if inspect.isawaitable(stop): - stop = await stop if stop: return event - async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None: + def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None: for hook, evfilter in self._hooks.get(filter_type, []): - if await evfilter.filter(event): + if evfilter.filter(event): try: - await hook(event) + hook(event) except Exception as ex: self.logger.exception(ex) - async def _parse_command(self, event: AttrDict) -> None: + def _parse_command(self, event: AttrDict) -> None: cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command] parts = event.message_snapshot.text.split(maxsplit=1) payload = parts[1] if len(parts) > 1 else "" cmd = parts.pop(0) if "@" in cmd: - suffix = "@" + (await self.account.self_contact.get_snapshot()).address + suffix = "@" + self.account.self_contact.get_snapshot().address if cmd.endswith(suffix): cmd = cmd[: -len(suffix)] else: @@ -153,32 +149,32 @@ class Client: event["command"], event["payload"] = cmd, payload - async def _on_new_msg(self, snapshot: AttrDict) -> None: + def _on_new_msg(self, snapshot: AttrDict) -> None: event = AttrDict(command="", payload="", message_snapshot=snapshot) if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX): - await self._parse_command(event) - await self._on_event(event, NewMessage) + self._parse_command(event) + self._on_event(event, NewMessage) - async def _handle_info_msg(self, snapshot: AttrDict) -> None: + def _handle_info_msg(self, snapshot: AttrDict) -> None: event = AttrDict(message_snapshot=snapshot) img_changed = parse_system_image_changed(snapshot.text) if img_changed: _, event["image_deleted"] = img_changed - await self._on_event(event, GroupImageChanged) + self._on_event(event, GroupImageChanged) return title_changed = parse_system_title_changed(snapshot.text) if title_changed: _, event["old_name"] = title_changed - await self._on_event(event, GroupNameChanged) + self._on_event(event, GroupNameChanged) return members_changed = parse_system_add_remove(snapshot.text) if members_changed: action, event["member"], _ = members_changed event["member_added"] = action == "added" - await self._on_event(event, MemberListChanged) + self._on_event(event, MemberListChanged) return self.logger.warning( @@ -187,20 +183,20 @@ class Client: snapshot.text, ) - async def _process_messages(self) -> None: + def _process_messages(self) -> None: if self._should_process_messages: - for message in await self.account.get_next_messages(): - snapshot = await message.get_snapshot() + for message in self.account.get_next_messages(): + snapshot = message.get_snapshot() if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]: - await self._on_new_msg(snapshot) + self._on_new_msg(snapshot) if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE: - await self._handle_info_msg(snapshot) - await snapshot.message.mark_seen() + self._handle_info_msg(snapshot) + snapshot.message.mark_seen() class Bot(Client): """Simple bot implementation that listent to events of a single account.""" - async def configure(self, email: str, password: str, **kwargs) -> None: + def configure(self, email: str, password: str, **kwargs) -> None: kwargs.setdefault("bot", "1") - await super().configure(email, password, **kwargs) + super().configure(email, password, **kwargs) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/const.py b/deltachat-rpc-client/src/deltachat_rpc_client/const.py index 3ca606617..c17ca8637 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/const.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/const.py @@ -45,6 +45,7 @@ class EventType(str, Enum): MSG_DELIVERED = "MsgDelivered" MSG_FAILED = "MsgFailed" MSG_READ = "MsgRead" + MSG_DELETED = "MsgDeleted" CHAT_MODIFIED = "ChatModified" CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified" CONTACTS_CHANGED = "ContactsChanged" diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py index efb3e9297..8f3c09d7f 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py @@ -24,39 +24,39 @@ class Contact: def _rpc(self) -> "Rpc": return self.account._rpc - async def block(self) -> None: + def block(self) -> None: """Block contact.""" - await self._rpc.block_contact(self.account.id, self.id) + self._rpc.block_contact(self.account.id, self.id) - async def unblock(self) -> None: + def unblock(self) -> None: """Unblock contact.""" - await self._rpc.unblock_contact(self.account.id, self.id) + self._rpc.unblock_contact(self.account.id, self.id) - async def delete(self) -> None: + def delete(self) -> None: """Delete contact.""" - await self._rpc.delete_contact(self.account.id, self.id) + self._rpc.delete_contact(self.account.id, self.id) - async def set_name(self, name: str) -> None: + def set_name(self, name: str) -> None: """Change the name of this contact.""" - await self._rpc.change_contact_name(self.account.id, self.id, name) + self._rpc.change_contact_name(self.account.id, self.id, name) - async def get_encryption_info(self) -> str: + def get_encryption_info(self) -> str: """Get a multi-line encryption info, containing your fingerprint and the fingerprint of the contact. """ - return await self._rpc.get_contact_encryption_info(self.account.id, self.id) + return self._rpc.get_contact_encryption_info(self.account.id, self.id) - async def get_snapshot(self) -> AttrDict: + def get_snapshot(self) -> AttrDict: """Return a dictionary with a snapshot of all contact properties.""" - snapshot = AttrDict(await self._rpc.get_contact(self.account.id, self.id)) + snapshot = AttrDict(self._rpc.get_contact(self.account.id, self.id)) snapshot["contact"] = self return snapshot - async def create_chat(self) -> "Chat": + def create_chat(self) -> "Chat": """Create or get an existing 1:1 chat for this contact.""" from .chat import Chat return Chat( self.account, - await self._rpc.create_chat_by_contact_id(self.account.id, self.id), + self._rpc.create_chat_by_contact_id(self.account.id, self.id), ) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py index c2cecd60d..ec3ed2d76 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py @@ -16,34 +16,34 @@ class DeltaChat: def __init__(self, rpc: "Rpc") -> None: self.rpc = rpc - async def add_account(self) -> Account: + def add_account(self) -> Account: """Create a new account database.""" - account_id = await self.rpc.add_account() + account_id = self.rpc.add_account() return Account(self, account_id) - async def get_all_accounts(self) -> List[Account]: + def get_all_accounts(self) -> List[Account]: """Return a list of all available accounts.""" - account_ids = await self.rpc.get_all_account_ids() + account_ids = self.rpc.get_all_account_ids() return [Account(self, account_id) for account_id in account_ids] - async def start_io(self) -> None: + def start_io(self) -> None: """Start the I/O of all accounts.""" - await self.rpc.start_io_for_all_accounts() + self.rpc.start_io_for_all_accounts() - async def stop_io(self) -> None: + def stop_io(self) -> None: """Stop the I/O of all accounts.""" - await self.rpc.stop_io_for_all_accounts() + self.rpc.stop_io_for_all_accounts() - async def maybe_network(self) -> None: + def maybe_network(self) -> None: """Indicate that the network likely has come back or just that the network conditions might have changed. """ - await self.rpc.maybe_network() + self.rpc.maybe_network() - async def get_system_info(self) -> AttrDict: + def get_system_info(self) -> AttrDict: """Get information about the Delta Chat core in this system.""" - return AttrDict(await self.rpc.get_system_info()) + return AttrDict(self.rpc.get_system_info()) - async def set_translations(self, translations: Dict[str, str]) -> None: + def set_translations(self, translations: Dict[str, str]) -> None: """Set stock translation strings.""" - await self.rpc.set_stock_strings(translations) + self.rpc.set_stock_strings(translations) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/events.py b/deltachat-rpc-client/src/deltachat_rpc_client/events.py index 4896527b9..b90b6e045 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/events.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/events.py @@ -1,5 +1,4 @@ """High-level classes for event processing and filtering.""" -import inspect import re from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union @@ -24,7 +23,7 @@ def _tuple_of(obj, type_: type) -> tuple: class EventFilter(ABC): """The base event filter. - :param func: A Callable (async or not) function that should accept the event as input + :param func: A Callable function that should accept the event as input parameter, and return a bool value indicating whether the event should be dispatched or not. """ @@ -43,16 +42,13 @@ class EventFilter(ABC): def __ne__(self, other): return not self == other - async def _call_func(self, event) -> bool: + def _call_func(self, event) -> bool: if not self.func: return True - res = self.func(event) - if inspect.isawaitable(res): - return await res - return res + return self.func(event) @abstractmethod - async def filter(self, event): + def filter(self, event): """Return True-like value if the event passed the filter and should be used, or False-like value otherwise. """ @@ -62,7 +58,7 @@ class RawEvent(EventFilter): """Matches raw core events. :param types: The types of event to match. - :param func: A Callable (async or not) function that should accept the event as input + :param func: A Callable function that should accept the event as input parameter, and return a bool value indicating whether the event should be dispatched or not. """ @@ -82,10 +78,10 @@ class RawEvent(EventFilter): return (self.types, self.func) == (other.types, other.func) return False - async def filter(self, event: "AttrDict") -> bool: - if self.types and event.type not in self.types: + def filter(self, event: "AttrDict") -> bool: + if self.types and event.kind not in self.types: return False - return await self._call_func(event) + return self._call_func(event) class NewMessage(EventFilter): @@ -104,7 +100,7 @@ class NewMessage(EventFilter): :param is_info: If set to True only match info/system messages, if set to False only match messages that are not info/system messages. If omitted info/system messages as well as normal messages will be matched. - :param func: A Callable (async or not) function that should accept the event as input + :param func: A Callable function that should accept the event as input parameter, and return a bool value indicating whether the event should be dispatched or not. """ @@ -159,7 +155,7 @@ class NewMessage(EventFilter): ) return False - async def filter(self, event: "AttrDict") -> bool: + def filter(self, event: "AttrDict") -> bool: if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot: return False if self.is_info is not None and self.is_info != event.message_snapshot.is_info: @@ -168,11 +164,9 @@ class NewMessage(EventFilter): return False if self.pattern: match = self.pattern(event.message_snapshot.text) - if inspect.isawaitable(match): - match = await match if not match: return False - return await super()._call_func(event) + return super()._call_func(event) class MemberListChanged(EventFilter): @@ -184,7 +178,7 @@ class MemberListChanged(EventFilter): :param added: If set to True only match if a member was added, if set to False only match if a member was removed. If omitted both, member additions and removals, will be matched. - :param func: A Callable (async or not) function that should accept the event as input + :param func: A Callable function that should accept the event as input parameter, and return a bool value indicating whether the event should be dispatched or not. """ @@ -201,10 +195,10 @@ class MemberListChanged(EventFilter): return (self.added, self.func) == (other.added, other.func) return False - async def filter(self, event: "AttrDict") -> bool: + def filter(self, event: "AttrDict") -> bool: if self.added is not None and self.added != event.member_added: return False - return await self._call_func(event) + return self._call_func(event) class GroupImageChanged(EventFilter): @@ -216,7 +210,7 @@ class GroupImageChanged(EventFilter): :param deleted: If set to True only match if the image was deleted, if set to False only match if a new image was set. If omitted both, image changes and removals, will be matched. - :param func: A Callable (async or not) function that should accept the event as input + :param func: A Callable function that should accept the event as input parameter, and return a bool value indicating whether the event should be dispatched or not. """ @@ -233,10 +227,10 @@ class GroupImageChanged(EventFilter): return (self.deleted, self.func) == (other.deleted, other.func) return False - async def filter(self, event: "AttrDict") -> bool: + def filter(self, event: "AttrDict") -> bool: if self.deleted is not None and self.deleted != event.image_deleted: return False - return await self._call_func(event) + return self._call_func(event) class GroupNameChanged(EventFilter): @@ -245,7 +239,7 @@ class GroupNameChanged(EventFilter): Warning: registering a handler for this event will cause the messages to be marked as read. Its usage is mainly intended for bots. - :param func: A Callable (async or not) function that should accept the event as input + :param func: A Callable function that should accept the event as input parameter, and return a bool value indicating whether the event should be dispatched or not. """ @@ -258,8 +252,8 @@ class GroupNameChanged(EventFilter): return self.func == other.func return False - async def filter(self, event: "AttrDict") -> bool: - return await self._call_func(event) + def filter(self, event: "AttrDict") -> bool: + return self._call_func(event) class HookCollection: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/message.py b/deltachat-rpc-client/src/deltachat_rpc_client/message.py index 5ec30961a..e728e690f 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/message.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/message.py @@ -1,6 +1,6 @@ import json from dataclasses import dataclass -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Optional, Union from ._utils import AttrDict from .contact import Contact @@ -21,32 +21,39 @@ class Message: def _rpc(self) -> "Rpc": return self.account._rpc - async def send_reaction(self, *reaction: str): + def send_reaction(self, *reaction: str): """Send a reaction to this message.""" - await self._rpc.send_reaction(self.account.id, self.id, reaction) + self._rpc.send_reaction(self.account.id, self.id, reaction) - async def get_snapshot(self) -> AttrDict: + def get_snapshot(self) -> AttrDict: """Get a snapshot with the properties of this message.""" from .chat import Chat - snapshot = AttrDict(await self._rpc.get_message(self.account.id, self.id)) + snapshot = AttrDict(self._rpc.get_message(self.account.id, self.id)) snapshot["chat"] = Chat(self.account, snapshot.chat_id) snapshot["sender"] = Contact(self.account, snapshot.from_id) snapshot["message"] = self return snapshot - async def mark_seen(self) -> None: - """Mark the message as seen.""" - await self._rpc.markseen_msgs(self.account.id, [self.id]) + def get_reactions(self) -> Optional[AttrDict]: + """Get message reactions.""" + reactions = self._rpc.get_message_reactions(self.account.id, self.id) + if reactions: + return AttrDict(reactions) + return None - async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None: + def mark_seen(self) -> None: + """Mark the message as seen.""" + self._rpc.markseen_msgs(self.account.id, [self.id]) + + def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None: """Send a webxdc status update. This message must be a webxdc.""" if not isinstance(update, str): update = json.dumps(update) - await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description) + self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description) - async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list: - return json.loads(await self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial)) + def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list: + return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial)) - async def get_webxdc_info(self) -> dict: - return await self._rpc.get_webxdc_info(self.account.id, self.id) + def get_webxdc_info(self) -> dict: + return self._rpc.get_webxdc_info(self.account.id, self.id) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index 86ae86afd..4d569d3ea 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -1,71 +1,67 @@ -import asyncio import json import os +import urllib.request from typing import AsyncGenerator, List, Optional -import aiohttp -import pytest_asyncio +import pytest from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message from .rpc import Rpc -async def get_temp_credentials() -> dict: +def get_temp_credentials() -> dict: url = os.getenv("DCC_NEW_TMP_EMAIL") assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set" - # Replace default 5 minute timeout with a 1 minute timeout. - timeout = aiohttp.ClientTimeout(total=60) - async with aiohttp.ClientSession() as session: - async with session.post(url, timeout=timeout) as response: - return json.loads(await response.text()) + request = urllib.request.Request(url, method="POST") + with urllib.request.urlopen(request, timeout=60) as f: + return json.load(f) class ACFactory: def __init__(self, deltachat: DeltaChat) -> None: self.deltachat = deltachat - async def get_unconfigured_account(self) -> Account: - return await self.deltachat.add_account() + def get_unconfigured_account(self) -> Account: + return self.deltachat.add_account() - async def get_unconfigured_bot(self) -> Bot: - return Bot(await self.get_unconfigured_account()) + def get_unconfigured_bot(self) -> Bot: + return Bot(self.get_unconfigured_account()) - async def new_preconfigured_account(self) -> Account: + def new_preconfigured_account(self) -> Account: """Make a new account with configuration options set, but configuration not started.""" - credentials = await get_temp_credentials() - account = await self.get_unconfigured_account() - await account.set_config("addr", credentials["email"]) - await account.set_config("mail_pw", credentials["password"]) - assert not await account.is_configured() + credentials = get_temp_credentials() + account = self.get_unconfigured_account() + account.set_config("addr", credentials["email"]) + account.set_config("mail_pw", credentials["password"]) + assert not account.is_configured() return account - async def new_configured_account(self) -> Account: - account = await self.new_preconfigured_account() - await account.configure() - assert await account.is_configured() + def new_configured_account(self) -> Account: + account = self.new_preconfigured_account() + account.configure() + assert account.is_configured() return account - async def new_configured_bot(self) -> Bot: - credentials = await get_temp_credentials() - bot = await self.get_unconfigured_bot() - await bot.configure(credentials["email"], credentials["password"]) + def new_configured_bot(self) -> Bot: + credentials = get_temp_credentials() + bot = self.get_unconfigured_bot() + bot.configure(credentials["email"], credentials["password"]) return bot - async def get_online_account(self) -> Account: - account = await self.new_configured_account() - await account.start_io() + def get_online_account(self) -> Account: + account = self.new_configured_account() + account.start_io() while True: - event = await account.wait_for_event() - print(event) - if event.type == EventType.IMAP_INBOX_IDLE: + event = account.wait_for_event() + if event.kind == EventType.IMAP_INBOX_IDLE: break return account - async def get_online_accounts(self, num: int) -> List[Account]: - return await asyncio.gather(*[self.get_online_account() for _ in range(num)]) + def get_online_accounts(self, num: int) -> List[Account]: + return [self.get_online_account() for _ in range(num)] - async def send_message( + def send_message( self, to_account: Account, from_account: Optional[Account] = None, @@ -74,16 +70,16 @@ class ACFactory: group: Optional[str] = None, ) -> Message: if not from_account: - from_account = (await self.get_online_accounts(1))[0] - to_contact = await from_account.create_contact(await to_account.get_config("addr")) + from_account = (self.get_online_accounts(1))[0] + to_contact = from_account.create_contact(to_account.get_config("addr")) if group: - to_chat = await from_account.create_group(group) - await to_chat.add_contact(to_contact) + to_chat = from_account.create_group(group) + to_chat.add_contact(to_contact) else: - to_chat = await to_contact.create_chat() - return await to_chat.send_message(text=text, file=file) + to_chat = to_contact.create_chat() + return to_chat.send_message(text=text, file=file) - async def process_message( + def process_message( self, to_client: Client, from_account: Optional[Account] = None, @@ -91,7 +87,7 @@ class ACFactory: file: Optional[str] = None, group: Optional[str] = None, ) -> AttrDict: - await self.send_message( + self.send_message( to_account=to_client.account, from_account=from_account, text=text, @@ -99,16 +95,16 @@ class ACFactory: group=group, ) - return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG) + return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG) -@pytest_asyncio.fixture -async def rpc(tmp_path) -> AsyncGenerator: +@pytest.fixture() +def rpc(tmp_path) -> AsyncGenerator: rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts")) - async with rpc_server: + with rpc_server: yield rpc_server -@pytest_asyncio.fixture -async def acfactory(rpc) -> AsyncGenerator: - yield ACFactory(DeltaChat(rpc)) +@pytest.fixture() +def acfactory(rpc) -> AsyncGenerator: + return ACFactory(DeltaChat(rpc)) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index f15c1a29a..4a181c528 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -1,6 +1,10 @@ -import asyncio import json +import logging import os +import subprocess +import sys +from queue import Queue +from threading import Event, Thread from typing import Any, Dict, Optional @@ -10,7 +14,7 @@ class JsonRpcError(Exception): class Rpc: def __init__(self, accounts_dir: Optional[str] = None, **kwargs): - """The given arguments will be passed to asyncio.create_subprocess_exec()""" + """The given arguments will be passed to subprocess.Popen()""" if accounts_dir: kwargs["env"] = { **kwargs.get("env", os.environ), @@ -18,81 +22,142 @@ class Rpc: } self._kwargs = kwargs - self.process: asyncio.subprocess.Process + self.process: subprocess.Popen self.id: int - self.event_queues: Dict[int, asyncio.Queue] - # Map from request ID to `asyncio.Future` returning the response. - self.request_events: Dict[int, asyncio.Future] - self.reader_task: asyncio.Task + self.event_queues: Dict[int, Queue] + # Map from request ID to `threading.Event`. + self.request_events: Dict[int, Event] + # Map from request ID to the result. + self.request_results: Dict[int, Any] + self.request_queue: Queue[Any] + self.closing: bool + self.reader_thread: Thread + self.writer_thread: Thread + self.events_thread: Thread - async def start(self) -> None: - self.process = await asyncio.create_subprocess_exec( - "deltachat-rpc-server", - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - **self._kwargs, - ) + def start(self) -> None: + if sys.version_info >= (3, 11): + self.process = subprocess.Popen( + "deltachat-rpc-server", + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + # Prevent subprocess from capturing SIGINT. + process_group=0, + **self._kwargs, + ) + else: + self.process = subprocess.Popen( + "deltachat-rpc-server", + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + # `process_group` is not supported before Python 3.11. + preexec_fn=os.setpgrp, # noqa: PLW1509 + **self._kwargs, + ) self.id = 0 self.event_queues = {} self.request_events = {} - self.reader_task = asyncio.create_task(self.reader_loop()) + self.request_results = {} + self.request_queue = Queue() + self.closing = False + self.reader_thread = Thread(target=self.reader_loop) + self.reader_thread.start() + self.writer_thread = Thread(target=self.writer_loop) + self.writer_thread.start() + self.events_thread = Thread(target=self.events_loop) + self.events_thread.start() - async def close(self) -> None: + def close(self) -> None: """Terminate RPC server process and wait until the reader loop finishes.""" - self.process.terminate() - await self.reader_task + self.closing = True + self.stop_io_for_all_accounts() + self.events_thread.join() + self.process.stdin.close() + self.reader_thread.join() + self.request_queue.put(None) + self.writer_thread.join() - async def __aenter__(self): - await self.start() + def __enter__(self): + self.start() return self - async def __aexit__(self, _exc_type, _exc, _tb): - await self.close() + def __exit__(self, _exc_type, _exc, _tb): + self.close() - async def reader_loop(self) -> None: - while True: - line = await self.process.stdout.readline() # noqa - if not line: # EOF - break - response = json.loads(line) - if "id" in response: - fut = self.request_events.pop(response["id"]) - fut.set_result(response) - elif response["method"] == "event": - # An event notification. - params = response["params"] - account_id = params["contextId"] - if account_id not in self.event_queues: - self.event_queues[account_id] = asyncio.Queue() - await self.event_queues[account_id].put(params["event"]) - else: - print(response) + def reader_loop(self) -> None: + try: + while True: + line = self.process.stdout.readline() + if not line: # EOF + break + response = json.loads(line) + if "id" in response: + response_id = response["id"] + event = self.request_events.pop(response_id) + self.request_results[response_id] = response + event.set() + else: + logging.warning("Got a response without ID: %s", response) + except Exception: + # Log an exception if the reader loop dies. + logging.exception("Exception in the reader loop") - async def wait_for_event(self, account_id: int) -> Optional[dict]: + def writer_loop(self) -> None: + """Writer loop ensuring only a single thread writes requests.""" + try: + while True: + request = self.request_queue.get() + if not request: + break + data = (json.dumps(request) + "\n").encode() + self.process.stdin.write(data) + self.process.stdin.flush() + + except Exception: + # Log an exception if the writer loop dies. + logging.exception("Exception in the writer loop") + + def get_queue(self, account_id: int) -> Queue: + if account_id not in self.event_queues: + self.event_queues[account_id] = Queue() + return self.event_queues[account_id] + + def events_loop(self) -> None: + """Requests new events and distributes them between queues.""" + try: + while True: + if self.closing: + return + event = self.get_next_event() + account_id = event["contextId"] + queue = self.get_queue(account_id) + queue.put(event["event"]) + except Exception: + # Log an exception if the event loop dies. + logging.exception("Exception in the event loop") + + def wait_for_event(self, account_id: int) -> Optional[dict]: """Waits for the next event from the given account and returns it.""" - if account_id in self.event_queues: - return await self.event_queues[account_id].get() - return None + queue = self.get_queue(account_id) + return queue.get() def __getattr__(self, attr: str): - async def method(*args, **kwargs) -> Any: + def method(*args) -> Any: self.id += 1 request_id = self.id - assert not (args and kwargs), "Mixing positional and keyword arguments" - request = { "jsonrpc": "2.0", "method": attr, - "params": kwargs or args, + "params": args, "id": self.id, } - data = (json.dumps(request) + "\n").encode() - self.process.stdin.write(data) # noqa - loop = asyncio.get_running_loop() - fut = loop.create_future() - self.request_events[request_id] = fut - response = await fut + event = Event() + self.request_events[request_id] = event + self.request_queue.put(request) + event.wait() + + response = self.request_results.pop(request_id) if "error" in response: raise JsonRpcError(response["error"]) if "result" in response: diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index d17c28a16..f91f9844e 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -1,4 +1,6 @@ -import asyncio +import concurrent.futures +import json +import subprocess from unittest.mock import MagicMock import pytest @@ -6,26 +8,26 @@ from deltachat_rpc_client import EventType, events from deltachat_rpc_client.rpc import JsonRpcError -@pytest.mark.asyncio() -async def test_system_info(rpc) -> None: - system_info = await rpc.get_system_info() +def test_system_info(rpc) -> None: + system_info = rpc.get_system_info() assert "arch" in system_info assert "deltachat_core_version" in system_info -@pytest.mark.asyncio() -async def test_sleep(rpc) -> None: +def test_sleep(rpc) -> None: """Test that long-running task does not block short-running task from completion.""" - sleep_5_task = asyncio.create_task(rpc.sleep(5.0)) - sleep_3_task = asyncio.create_task(rpc.sleep(3.0)) - done, pending = await asyncio.wait([sleep_5_task, sleep_3_task], return_when=asyncio.FIRST_COMPLETED) - assert sleep_3_task in done - assert sleep_5_task in pending - sleep_5_task.cancel() + with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: + sleep_5_future = executor.submit(rpc.sleep, 5.0) + sleep_3_future = executor.submit(rpc.sleep, 3.0) + done, pending = concurrent.futures.wait( + [sleep_5_future, sleep_3_future], + return_when=concurrent.futures.FIRST_COMPLETED, + ) + assert sleep_3_future in done + assert sleep_5_future in pending -@pytest.mark.asyncio() -async def test_email_address_validity(rpc) -> None: +def test_email_address_validity(rpc) -> None: valid_addresses = [ "email@example.com", "36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail", @@ -33,17 +35,16 @@ async def test_email_address_validity(rpc) -> None: invalid_addresses = ["email@", "example.com", "emai221"] for addr in valid_addresses: - assert await rpc.check_email_validity(addr) + assert rpc.check_email_validity(addr) for addr in invalid_addresses: - assert not await rpc.check_email_validity(addr) + assert not rpc.check_email_validity(addr) -@pytest.mark.asyncio() -async def test_acfactory(acfactory) -> None: - account = await acfactory.new_configured_account() +def test_acfactory(acfactory) -> None: + account = acfactory.new_configured_account() while True: - event = await account.wait_for_event() - if event.type == EventType.CONFIGURE_PROGRESS: + event = account.wait_for_event() + if event.kind == EventType.CONFIGURE_PROGRESS: assert event.progress != 0 # Progress 0 indicates error. if event.progress == 1000: # Success break @@ -52,234 +53,241 @@ async def test_acfactory(acfactory) -> None: print("Successful configuration") -@pytest.mark.asyncio() -async def test_configure_starttls(acfactory) -> None: - account = await acfactory.new_preconfigured_account() +def test_configure_starttls(acfactory) -> None: + account = acfactory.new_preconfigured_account() # Use STARTTLS - await account.set_config("mail_security", "2") - await account.set_config("send_security", "2") - await account.configure() - assert await account.is_configured() + account.set_config("mail_security", "2") + account.set_config("send_security", "2") + account.configure() + assert account.is_configured() -@pytest.mark.asyncio() -async def test_account(acfactory) -> None: - alice, bob = await acfactory.get_online_accounts(2) +def test_account(acfactory) -> None: + alice, bob = acfactory.get_online_accounts(2) - bob_addr = await bob.get_config("addr") - alice_contact_bob = await alice.create_contact(bob_addr, "Bob") - alice_chat_bob = await alice_contact_bob.create_chat() - await alice_chat_bob.send_text("Hello!") + bob_addr = bob.get_config("addr") + alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_chat_bob = alice_contact_bob.create_chat() + alice_chat_bob.send_text("Hello!") while True: - event = await bob.wait_for_event() - if event.type == EventType.INCOMING_MSG: + event = bob.wait_for_event() + if event.kind == EventType.INCOMING_MSG: chat_id = event.chat_id msg_id = event.msg_id break message = bob.get_message_by_id(msg_id) - snapshot = await message.get_snapshot() + snapshot = message.get_snapshot() assert snapshot.chat_id == chat_id assert snapshot.text == "Hello!" - await bob.mark_seen_messages([message]) + bob.mark_seen_messages([message]) assert alice != bob assert repr(alice) - assert (await alice.get_info()).level - assert await alice.get_size() - assert await alice.is_configured() - assert not await alice.get_avatar() - assert await alice.get_contact_by_addr(bob_addr) == alice_contact_bob - assert await alice.get_contacts() - assert await alice.get_contacts(snapshot=True) + assert alice.get_info().level + assert alice.get_size() + assert alice.is_configured() + assert not alice.get_avatar() + assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob + assert alice.get_contacts() + assert alice.get_contacts(snapshot=True) assert alice.self_contact - assert await alice.get_chatlist() - assert await alice.get_chatlist(snapshot=True) - assert await alice.get_qr_code() - assert await alice.get_fresh_messages() - assert await alice.get_next_messages() + assert alice.get_chatlist() + assert alice.get_chatlist(snapshot=True) + assert alice.get_qr_code() + assert alice.get_fresh_messages() + assert alice.get_next_messages() - group = await alice.create_group("test group") - await group.add_contact(alice_contact_bob) - group_msg = await group.send_message(text="hello") + # Test sending empty message. + assert len(bob.wait_next_messages()) == 0 + alice_chat_bob.send_text("") + messages = bob.wait_next_messages() + assert len(messages) == 1 + message = messages[0] + snapshot = message.get_snapshot() + assert snapshot.text == "" + bob.mark_seen_messages([message]) + + group = alice.create_group("test group") + group.add_contact(alice_contact_bob) + group_msg = group.send_message(text="hello") assert group_msg == alice.get_message_by_id(group_msg.id) assert group == alice.get_chat_by_id(group.id) - await alice.delete_messages([group_msg]) + alice.delete_messages([group_msg]) - await alice.set_config("selfstatus", "test") - assert await alice.get_config("selfstatus") == "test" - await alice.update_config(selfstatus="test2") - assert await alice.get_config("selfstatus") == "test2" + alice.set_config("selfstatus", "test") + assert alice.get_config("selfstatus") == "test" + alice.update_config(selfstatus="test2") + assert alice.get_config("selfstatus") == "test2" - assert not await alice.get_blocked_contacts() - await alice_contact_bob.block() - blocked_contacts = await alice.get_blocked_contacts() + assert not alice.get_blocked_contacts() + alice_contact_bob.block() + blocked_contacts = alice.get_blocked_contacts() assert blocked_contacts assert blocked_contacts[0].contact == alice_contact_bob - await bob.remove() - await alice.stop_io() + bob.remove() + alice.stop_io() -@pytest.mark.asyncio() -async def test_chat(acfactory) -> None: - alice, bob = await acfactory.get_online_accounts(2) +def test_chat(acfactory) -> None: + alice, bob = acfactory.get_online_accounts(2) - bob_addr = await bob.get_config("addr") - alice_contact_bob = await alice.create_contact(bob_addr, "Bob") - alice_chat_bob = await alice_contact_bob.create_chat() - await alice_chat_bob.send_text("Hello!") + bob_addr = bob.get_config("addr") + alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_chat_bob = alice_contact_bob.create_chat() + alice_chat_bob.send_text("Hello!") while True: - event = await bob.wait_for_event() - if event.type == EventType.INCOMING_MSG: + event = bob.wait_for_event() + if event.kind == EventType.INCOMING_MSG: chat_id = event.chat_id msg_id = event.msg_id break message = bob.get_message_by_id(msg_id) - snapshot = await message.get_snapshot() + snapshot = message.get_snapshot() assert snapshot.chat_id == chat_id assert snapshot.text == "Hello!" bob_chat_alice = bob.get_chat_by_id(chat_id) assert alice_chat_bob != bob_chat_alice assert repr(alice_chat_bob) - await alice_chat_bob.delete() - assert not await bob_chat_alice.can_send() - await bob_chat_alice.accept() - assert await bob_chat_alice.can_send() - await bob_chat_alice.block() - bob_chat_alice = await snapshot.sender.create_chat() - await bob_chat_alice.mute() - await bob_chat_alice.unmute() - await bob_chat_alice.pin() - await bob_chat_alice.unpin() - await bob_chat_alice.archive() - await bob_chat_alice.unarchive() + alice_chat_bob.delete() + assert not bob_chat_alice.can_send() + bob_chat_alice.accept() + assert bob_chat_alice.can_send() + bob_chat_alice.block() + bob_chat_alice = snapshot.sender.create_chat() + bob_chat_alice.mute() + bob_chat_alice.unmute() + bob_chat_alice.pin() + bob_chat_alice.unpin() + bob_chat_alice.archive() + bob_chat_alice.unarchive() with pytest.raises(JsonRpcError): # can't set name for 1:1 chats - await bob_chat_alice.set_name("test") - await bob_chat_alice.set_ephemeral_timer(300) - await bob_chat_alice.get_encryption_info() + bob_chat_alice.set_name("test") + bob_chat_alice.set_ephemeral_timer(300) + bob_chat_alice.get_encryption_info() - group = await alice.create_group("test group") - await group.add_contact(alice_contact_bob) - await group.get_qr_code() + group = alice.create_group("test group") + group.add_contact(alice_contact_bob) + group.get_qr_code() - snapshot = await group.get_basic_snapshot() + snapshot = group.get_basic_snapshot() assert snapshot.name == "test group" - await group.set_name("new name") - snapshot = await group.get_full_snapshot() + group.set_name("new name") + snapshot = group.get_full_snapshot() assert snapshot.name == "new name" - msg = await group.send_message(text="hi") - assert (await msg.get_snapshot()).text == "hi" - await group.forward_messages([msg]) + msg = group.send_message(text="hi") + assert (msg.get_snapshot()).text == "hi" + group.forward_messages([msg]) - await group.set_draft(text="test draft") - draft = await group.get_draft() + group.set_draft(text="test draft") + draft = group.get_draft() assert draft.text == "test draft" - await group.remove_draft() - assert not await group.get_draft() + group.remove_draft() + assert not group.get_draft() - assert await group.get_messages() - await group.get_fresh_message_count() - await group.mark_noticed() - assert await group.get_contacts() - await group.remove_contact(alice_chat_bob) - await group.get_locations() + assert group.get_messages() + group.get_fresh_message_count() + group.mark_noticed() + assert group.get_contacts() + group.remove_contact(alice_chat_bob) + group.get_locations() -@pytest.mark.asyncio() -async def test_contact(acfactory) -> None: - alice, bob = await acfactory.get_online_accounts(2) +def test_contact(acfactory) -> None: + alice, bob = acfactory.get_online_accounts(2) - bob_addr = await bob.get_config("addr") - alice_contact_bob = await alice.create_contact(bob_addr, "Bob") + bob_addr = bob.get_config("addr") + alice_contact_bob = alice.create_contact(bob_addr, "Bob") assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id) assert repr(alice_contact_bob) - await alice_contact_bob.block() - await alice_contact_bob.unblock() - await alice_contact_bob.set_name("new name") - await alice_contact_bob.get_encryption_info() - snapshot = await alice_contact_bob.get_snapshot() + alice_contact_bob.block() + alice_contact_bob.unblock() + alice_contact_bob.set_name("new name") + alice_contact_bob.get_encryption_info() + snapshot = alice_contact_bob.get_snapshot() assert snapshot.address == bob_addr - await alice_contact_bob.create_chat() + alice_contact_bob.create_chat() -@pytest.mark.asyncio() -async def test_message(acfactory) -> None: - alice, bob = await acfactory.get_online_accounts(2) +def test_message(acfactory) -> None: + alice, bob = acfactory.get_online_accounts(2) - bob_addr = await bob.get_config("addr") - alice_contact_bob = await alice.create_contact(bob_addr, "Bob") - alice_chat_bob = await alice_contact_bob.create_chat() - await alice_chat_bob.send_text("Hello!") + bob_addr = bob.get_config("addr") + alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_chat_bob = alice_contact_bob.create_chat() + alice_chat_bob.send_text("Hello!") while True: - event = await bob.wait_for_event() - if event.type == EventType.INCOMING_MSG: + event = bob.wait_for_event() + if event.kind == EventType.INCOMING_MSG: chat_id = event.chat_id msg_id = event.msg_id break message = bob.get_message_by_id(msg_id) - snapshot = await message.get_snapshot() + snapshot = message.get_snapshot() assert snapshot.chat_id == chat_id assert snapshot.text == "Hello!" assert not snapshot.is_bot assert repr(message) with pytest.raises(JsonRpcError): # chat is not accepted - await snapshot.chat.send_text("hi") - await snapshot.chat.accept() - await snapshot.chat.send_text("hi") + snapshot.chat.send_text("hi") + snapshot.chat.accept() + snapshot.chat.send_text("hi") - await message.mark_seen() - await message.send_reaction("😎") + message.mark_seen() + message.send_reaction("😎") + reactions = message.get_reactions() + assert reactions + snapshot = message.get_snapshot() + assert reactions == snapshot.reactions -@pytest.mark.asyncio() -async def test_is_bot(acfactory) -> None: +def test_is_bot(acfactory) -> None: """Test that we can recognize messages submitted by bots.""" - alice, bob = await acfactory.get_online_accounts(2) + alice, bob = acfactory.get_online_accounts(2) - bob_addr = await bob.get_config("addr") - alice_contact_bob = await alice.create_contact(bob_addr, "Bob") - alice_chat_bob = await alice_contact_bob.create_chat() + bob_addr = bob.get_config("addr") + alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_chat_bob = alice_contact_bob.create_chat() # Alice becomes a bot. - await alice.set_config("bot", "1") - await alice_chat_bob.send_text("Hello!") + alice.set_config("bot", "1") + alice_chat_bob.send_text("Hello!") while True: - event = await bob.wait_for_event() - if event.type == EventType.INCOMING_MSG: + event = bob.wait_for_event() + if event.kind == EventType.INCOMING_MSG: msg_id = event.msg_id message = bob.get_message_by_id(msg_id) - snapshot = await message.get_snapshot() + snapshot = message.get_snapshot() assert snapshot.chat_id == event.chat_id assert snapshot.text == "Hello!" assert snapshot.is_bot break -@pytest.mark.asyncio() -async def test_bot(acfactory) -> None: +def test_bot(acfactory) -> None: mock = MagicMock() - user = (await acfactory.get_online_accounts(1))[0] - bot = await acfactory.new_configured_bot() - bot2 = await acfactory.new_configured_bot() + user = (acfactory.get_online_accounts(1))[0] + bot = acfactory.new_configured_bot() + bot2 = acfactory.new_configured_bot() - assert await bot.is_configured() - assert await bot.account.get_config("bot") == "1" + assert bot.is_configured() + assert bot.account.get_config("bot") == "1" hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG) bot.add_hook(*hook) - event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!") - snapshot = await bot.account.get_message_by_id(event.msg_id).get_snapshot() + event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!") + snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot() assert not snapshot.is_bot mock.hook.assert_called_once_with(event.msg_id) bot.remove_hook(*hook) @@ -291,43 +299,62 @@ async def test_bot(acfactory) -> None: hook = track, events.NewMessage(r"hello") bot.add_hook(*hook) bot.add_hook(track, events.NewMessage(command="/help")) - event = await acfactory.process_message(from_account=user, to_client=bot, text="hello") + event = acfactory.process_message(from_account=user, to_client=bot, text="hello") mock.hook.assert_called_with(event.msg_id) - event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!") + event = acfactory.process_message(from_account=user, to_client=bot, text="hello!") mock.hook.assert_called_with(event.msg_id) - await acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello") + acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello") assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots - await acfactory.process_message(from_account=user, to_client=bot, text="hey!") + acfactory.process_message(from_account=user, to_client=bot, text="hey!") assert len(mock.hook.mock_calls) == 2 bot.remove_hook(*hook) mock.hook.reset_mock() - await acfactory.process_message(from_account=user, to_client=bot, text="hello") - event = await acfactory.process_message(from_account=user, to_client=bot, text="/help") + acfactory.process_message(from_account=user, to_client=bot, text="hello") + event = acfactory.process_message(from_account=user, to_client=bot, text="/help") mock.hook.assert_called_once_with(event.msg_id) -@pytest.mark.asyncio() -async def test_wait_next_messages(acfactory) -> None: - alice = await acfactory.new_configured_account() +def test_wait_next_messages(acfactory) -> None: + alice = acfactory.new_configured_account() # Create a bot account so it does not receive device messages in the beginning. - bot = await acfactory.new_preconfigured_account() - await bot.set_config("bot", "1") - await bot.configure() + bot = acfactory.new_preconfigured_account() + bot.set_config("bot", "1") + bot.configure() # There are no old messages and the call returns immediately. - assert not await bot.wait_next_messages() + assert not bot.wait_next_messages() - # Bot starts waiting for messages. - next_messages_task = asyncio.create_task(bot.wait_next_messages()) + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + # Bot starts waiting for messages. + next_messages_task = executor.submit(bot.wait_next_messages) - bot_addr = await bot.get_config("addr") - alice_contact_bot = await alice.create_contact(bot_addr, "Bob") - alice_chat_bot = await alice_contact_bot.create_chat() - await alice_chat_bot.send_text("Hello!") + bot_addr = bot.get_config("addr") + alice_contact_bot = alice.create_contact(bot_addr, "Bob") + alice_chat_bot = alice_contact_bot.create_chat() + alice_chat_bot.send_text("Hello!") - next_messages = await next_messages_task - assert len(next_messages) == 1 - snapshot = await next_messages[0].get_snapshot() - assert snapshot.text == "Hello!" + next_messages = next_messages_task.result() + assert len(next_messages) == 1 + snapshot = next_messages[0].get_snapshot() + assert snapshot.text == "Hello!" + + +def test_import_export(acfactory, tmp_path) -> None: + alice = acfactory.new_configured_account() + alice.export_backup(tmp_path) + + files = list(tmp_path.glob("*.tar")) + alice2 = acfactory.get_unconfigured_account() + alice2.import_backup(files[0]) + + assert alice2.manager.get_system_info() + + +def test_openrpc_command_line() -> None: + """Test that "deltachat-rpc-server --openrpc" command returns an OpenRPC specification.""" + out = subprocess.run(["deltachat-rpc-server", "--openrpc"], capture_output=True, check=True).stdout + openrpc = json.loads(out) + assert "openrpc" in openrpc + assert "methods" in openrpc diff --git a/deltachat-rpc-client/tests/test_webxdc.py b/deltachat-rpc-client/tests/test_webxdc.py index 8a0584d03..8f3d8edcb 100644 --- a/deltachat-rpc-client/tests/test_webxdc.py +++ b/deltachat-rpc-client/tests/test_webxdc.py @@ -1,24 +1,22 @@ -import pytest from deltachat_rpc_client import EventType -@pytest.mark.asyncio() -async def test_webxdc(acfactory) -> None: - alice, bob = await acfactory.get_online_accounts(2) +def test_webxdc(acfactory) -> None: + alice, bob = acfactory.get_online_accounts(2) - bob_addr = await bob.get_config("addr") - alice_contact_bob = await alice.create_contact(bob_addr, "Bob") - alice_chat_bob = await alice_contact_bob.create_chat() - await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc") + bob_addr = bob.get_config("addr") + alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_chat_bob = alice_contact_bob.create_chat() + alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc") while True: - event = await bob.wait_for_event() - if event.type == EventType.INCOMING_MSG: + event = bob.wait_for_event() + if event.kind == EventType.INCOMING_MSG: bob_chat_alice = bob.get_chat_by_id(event.chat_id) message = bob.get_message_by_id(event.msg_id) break - webxdc_info = await message.get_webxdc_info() + webxdc_info = message.get_webxdc_info() assert webxdc_info == { "document": None, "icon": "icon.png", @@ -28,20 +26,20 @@ async def test_webxdc(acfactory) -> None: "summary": None, } - status_updates = await message.get_webxdc_status_updates() + status_updates = message.get_webxdc_status_updates() assert status_updates == [] - await bob_chat_alice.accept() - await message.send_webxdc_status_update({"payload": 42}, "") - await message.send_webxdc_status_update({"payload": "Second update"}, "description") + bob_chat_alice.accept() + message.send_webxdc_status_update({"payload": 42}, "") + message.send_webxdc_status_update({"payload": "Second update"}, "description") - status_updates = await message.get_webxdc_status_updates() + status_updates = message.get_webxdc_status_updates() assert status_updates == [ {"payload": 42, "serial": 1, "max_serial": 2}, {"payload": "Second update", "serial": 2, "max_serial": 2}, ] - status_updates = await message.get_webxdc_status_updates(1) + status_updates = message.get_webxdc_status_updates(1) assert status_updates == [ {"payload": "Second update", "serial": 2, "max_serial": 2}, ] diff --git a/deltachat-rpc-client/tox.ini b/deltachat-rpc-client/tox.ini index 1d0dba56c..4bcdd7e0a 100644 --- a/deltachat-rpc-client/tox.ini +++ b/deltachat-rpc-client/tox.ini @@ -6,7 +6,7 @@ envlist = [testenv] commands = - pytest {posargs} + pytest -n6 {posargs} setenv = # Avoid stack overflow when Rust core is built without optimizations. RUST_MIN_STACK=8388608 @@ -14,10 +14,8 @@ passenv = DCC_NEW_TMP_EMAIL deps = pytest - pytest-asyncio pytest-timeout - aiohttp - aiodns + pytest-xdist [testenv:lint] skipsdist = True diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index f66223ade..d088dc614 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-rpc-server" -version = "1.112.6" +version = "1.126.1" description = "DeltaChat JSON-RPC server" edition = "2021" readme = "README.md" @@ -15,13 +15,13 @@ deltachat = { path = "..", default-features = false } anyhow = "1" env_logger = { version = "0.10.0" } -futures-lite = "1.12.0" +futures-lite = "1.13.0" log = "0.4" -serde_json = "1.0.95" +serde_json = "1.0.105" serde = { version = "1.0", features = ["derive"] } -tokio = { version = "1.27.0", features = ["io-std"] } -tokio-util = "0.7.7" -yerpc = { version = "0.4.0", features = ["anyhow_expose"] } +tokio = { version = "1.32.0", features = ["io-std"] } +tokio-util = "0.7.9" +yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] } [features] default = ["vendored"] diff --git a/deltachat-rpc-server/README.md b/deltachat-rpc-server/README.md index 2027072a5..be8c0c562 100644 --- a/deltachat-rpc-server/README.md +++ b/deltachat-rpc-server/README.md @@ -32,3 +32,6 @@ languages other than Rust, for example: 1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/ 2. Go: https://github.com/deltachat/deltachat-rpc-client-go/ + +Run `deltachat-rpc-server --version` to check the version of the server. +Run `deltachat-rpc-server --openrpc` to get [OpenRPC](https://open-rpc.org/) specification of the provided JSON-RPC API. diff --git a/deltachat-rpc-server/src/main.rs b/deltachat-rpc-server/src/main.rs index 87df5fdf4..4be58760b 100644 --- a/deltachat-rpc-server/src/main.rs +++ b/deltachat-rpc-server/src/main.rs @@ -1,23 +1,36 @@ +//! Delta Chat core RPC server. +//! +//! It speaks JSON Lines over stdio. use std::env; -///! Delta Chat core RPC server. -///! -///! It speaks JSON Lines over stdio. use std::path::PathBuf; use std::sync::Arc; use anyhow::{anyhow, Context as _, Result}; use deltachat::constants::DC_VERSION_STR; -use deltachat_jsonrpc::api::events::event_to_json_rpc_notification; use deltachat_jsonrpc::api::{Accounts, CommandApi}; use futures_lite::stream::StreamExt; use tokio::io::{self, AsyncBufReadExt, BufReader}; +use yerpc::RpcServer as _; + +#[cfg(target_family = "unix")] +use tokio::signal::unix as signal_unix; + use tokio::sync::RwLock; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use yerpc::{RpcClient, RpcSession}; #[tokio::main(flavor = "multi_thread")] -async fn main() -> Result<()> { +async fn main() { + let r = main_impl().await; + // From tokio documentation: + // "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate + // thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang + // until the user presses enter." + std::process::exit(if r.is_ok() { 0 } else { 1 }); +} + +async fn main_impl() -> Result<()> { let mut args = env::args_os(); let _program_name = args.next().context("no command line arguments found")?; if let Some(first_arg) = args.next() { @@ -27,6 +40,12 @@ async fn main() -> Result<()> { } eprintln!("{}", &*DC_VERSION_STR); return Ok(()); + } else if first_arg.to_str() == Some("--openrpc") { + if let Some(arg) = args.next() { + return Err(anyhow!("Unrecognized argument {:?}", arg)); + } + println!("{}", CommandApi::openrpc_specification()?); + return Ok(()); } else { return Err(anyhow!("Unrecognized option {:?}", first_arg)); } @@ -35,12 +54,17 @@ async fn main() -> Result<()> { return Err(anyhow!("Unrecognized argument {:?}", arg)); } + // Install signal handlers early so that the shutdown is graceful starting from here. + let _ctrl_c = tokio::signal::ctrl_c(); + #[cfg(target_family = "unix")] + let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?; + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string()); log::info!("Starting with accounts directory `{}`.", path); - let accounts = Accounts::new(PathBuf::from(&path)).await?; - let events = accounts.get_event_emitter(); + let writable = true; + let accounts = Accounts::new(PathBuf::from(&path), writable).await?; log::info!("Creating JSON-RPC API."); let accounts = Arc::new(RwLock::new(accounts)); @@ -48,28 +72,15 @@ async fn main() -> Result<()> { let (client, mut out_receiver) = RpcClient::new(); let session = RpcSession::new(client.clone(), state.clone()); - let canceler = CancellationToken::new(); - - // Events task converts core events to JSON-RPC notifications. - let events_task: JoinHandle> = tokio::spawn(async move { - let mut r = Ok(()); - while let Some(event) = events.recv().await { - if r.is_err() { - continue; - } - let event = event_to_json_rpc_notification(event); - r = client.send_notification("event", Some(event)).await; - } - r?; - Ok(()) - }); + let main_cancel = CancellationToken::new(); // Send task prints JSON responses to stdout. - let cancelable = canceler.clone(); + let cancel = main_cancel.clone(); let send_task: JoinHandle> = tokio::spawn(async move { + let _cancel_guard = cancel.clone().drop_guard(); loop { let message = tokio::select! { - _ = cancelable.cancelled() => break, + _ = cancel.cancelled() => break, message = out_receiver.next() => match message { None => break, Some(message) => serde_json::to_string(&message)?, @@ -81,12 +92,32 @@ async fn main() -> Result<()> { Ok(()) }); + let cancel = main_cancel.clone(); + let sigterm_task: JoinHandle> = tokio::spawn(async move { + #[cfg(target_family = "unix")] + { + let _cancel_guard = cancel.clone().drop_guard(); + tokio::select! { + _ = cancel.cancelled() => (), + _ = sigterm.recv() => { + log::info!("got SIGTERM"); + } + } + } + let _ = cancel; + Ok(()) + }); + // Receiver task reads JSON requests from stdin. + let cancel = main_cancel.clone(); let recv_task: JoinHandle> = tokio::spawn(async move { + let _cancel_guard = cancel.clone().drop_guard(); let stdin = io::stdin(); let mut lines = BufReader::new(stdin).lines(); + loop { let message = tokio::select! { + _ = cancel.cancelled() => break, _ = tokio::signal::ctrl_c() => { log::info!("got ctrl-c event"); break; @@ -108,18 +139,13 @@ async fn main() -> Result<()> { Ok(()) }); - // Wait for the end of stdin / ctrl-c. - recv_task.await?.ok(); - - // See "Thread safety" section in deltachat-ffi/deltachat.h for explanation. - // NB: Events are drained by events_task. - canceler.cancel(); + main_cancel.cancelled().await; accounts.read().await.stop_io().await; + drop(accounts); drop(state); - let (r0, r1) = tokio::join!(events_task, send_task); - for r in [r0, r1] { - r??; - } + send_task.await??; + sigterm_task.await??; + recv_task.await??; Ok(()) } diff --git a/deny.toml b/deny.toml index e2f70f17d..22d06bab4 100644 --- a/deny.toml +++ b/deny.toml @@ -2,9 +2,7 @@ unmaintained = "allow" ignore = [ "RUSTSEC-2020-0071", - - # Only affects windows if using non-default allocator (and unmaintained). - "RUSTSEC-2021-0145", + "RUSTSEC-2022-0093", ] [bans] @@ -17,8 +15,6 @@ skip = [ { name = "base64", version = "<0.21" }, { name = "bitflags", version = "1.3.2" }, { name = "block-buffer", version = "<0.10" }, - { name = "clap_lex", version = "0.2.4" }, - { name = "clap", version = "3.2.23" }, { name = "convert_case", version = "0.4.0" }, { name = "curve25519-dalek", version = "3.2.0" }, { name = "darling_core", version = "<0.14" }, @@ -28,12 +24,10 @@ skip = [ { name = "digest", version = "<0.10" }, { name = "ed25519-dalek", version = "1.0.1" }, { name = "ed25519", version = "1.5.3" }, - { name = "env_logger", version = "<0.10" }, + { name = "fastrand", version = "1.9.0" }, { name = "getrandom", version = "<0.2" }, - { name = "hermit-abi", version = "<0.3" }, - { name = "humantime", version = "<2.1" }, - { name = "idna", version = "<0.3" }, - { name = "libm", version = "0.1.4" }, + { name = "hashbrown", version = "<0.14.0" }, + { name = "indexmap", version = "<2.0.0" }, { name = "pem-rfc7468", version = "0.6.0" }, { name = "pkcs8", version = "0.9.0" }, { name = "quick-error", version = "<2.0" }, @@ -41,20 +35,27 @@ skip = [ { name = "rand_core", version = "<0.6" }, { name = "rand", version = "<0.8" }, { name = "redox_syscall", version = "0.2.16" }, + { name = "regex-automata", version = "0.1.10" }, + { name = "regex-syntax", version = "0.6.29" }, { name = "sec1", version = "0.3.0" }, { name = "sha2", version = "<0.10" }, { name = "signature", version = "1.6.4" }, + { name = "socket2", version = "0.4.9" }, { name = "spin", version = "<0.9.6" }, { name = "spki", version = "0.6.0" }, { name = "syn", version = "1.0.109" }, { name = "time", version = "<0.3" }, { name = "wasi", version = "<0.11" }, - { name = "windows_aarch64_msvc", version = "<0.42" }, - { name = "windows_i686_gnu", version = "<0.42" }, - { name = "windows_i686_msvc", version = "<0.42" }, - { name = "windows-sys", version = "<0.45" }, - { name = "windows_x86_64_gnu", version = "<0.42" }, - { name = "windows_x86_64_msvc", version = "<0.42" }, + { name = "windows_aarch64_gnullvm", version = "<0.48" }, + { name = "windows_aarch64_msvc", version = "<0.48" }, + { name = "windows_i686_gnu", version = "<0.48" }, + { name = "windows_i686_msvc", version = "<0.48" }, + { name = "windows-sys", version = "<0.48" }, + { name = "windows-targets", version = "<0.48" }, + { name = "windows", version = "0.32.0" }, + { name = "windows_x86_64_gnullvm", version = "<0.48" }, + { name = "windows_x86_64_gnu", version = "<0.48" }, + { name = "windows_x86_64_msvc", version = "<0.48" }, ] diff --git a/draft/aeap-mvp.md b/draft/aeap-mvp.md index 94ac3c03e..040125f57 100644 --- a/draft/aeap-mvp.md +++ b/draft/aeap-mvp.md @@ -108,7 +108,7 @@ The most obvious alternative would be to create a new contact with the new addre #### Upsides: - With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally. - (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.) -- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't wast that much development time.) +- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't waste that much development time.) [full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161) diff --git a/examples/simple.rs b/examples/simple.rs deleted file mode 100644 index cca50bb27..000000000 --- a/examples/simple.rs +++ /dev/null @@ -1,100 +0,0 @@ -use deltachat::chat::{self, ChatId}; -use deltachat::chatlist::*; -use deltachat::config; -use deltachat::contact::*; -use deltachat::context::*; -use deltachat::message::Message; -use deltachat::stock_str::StockStrings; -use deltachat::{EventType, Events}; -use tempfile::tempdir; - -fn cb(event: EventType) { - match event { - EventType::ConfigureProgress { progress, .. } => { - log::info!("progress: {}", progress); - } - EventType::Info(msg) => { - log::info!("{}", msg); - } - EventType::Warning(msg) => { - log::warn!("{}", msg); - } - EventType::Error(msg) => { - log::error!("{}", msg); - } - event => { - log::info!("{:?}", event); - } - } -} - -/// Run with `RUST_LOG=simple=info cargo run --release --example simple -- email pw`. -#[tokio::main] -async fn main() { - pretty_env_logger::try_init_timed().ok(); - - let dir = tempdir().unwrap(); - let dbfile = dir.path().join("db.sqlite"); - log::info!("creating database {:?}", dbfile); - let ctx = Context::new(&dbfile, 0, Events::new(), StockStrings::new()) - .await - .expect("Failed to create context"); - let info = ctx.get_info().await; - log::info!("info: {:#?}", info); - - let events = ctx.get_event_emitter(); - let events_spawn = tokio::task::spawn(async move { - while let Some(event) = events.recv().await { - cb(event.typ); - } - }); - - log::info!("configuring"); - let args = std::env::args().collect::>(); - assert_eq!(args.len(), 3, "requires email password"); - let email = args[1].clone(); - let pw = args[2].clone(); - ctx.set_config(config::Config::Addr, Some(&email)) - .await - .unwrap(); - ctx.set_config(config::Config::MailPw, Some(&pw)) - .await - .unwrap(); - - ctx.configure().await.unwrap(); - - log::info!("------ RUN ------"); - ctx.start_io().await; - log::info!("--- SENDING A MESSAGE ---"); - - let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com") - .await - .unwrap(); - let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap(); - - for i in 0..1 { - log::info!("sending message {}", i); - chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {i}nth message!")) - .await - .unwrap(); - } - - // wait for the message to be sent out - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - - log::info!("fetching chats.."); - let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap(); - - for i in 0..chats.len() { - let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap()) - .await - .unwrap(); - log::info!("[{}] msg: {:?}", i, msg); - } - - log::info!("stopping"); - ctx.stop_io().await; - log::info!("closing"); - drop(ctx); - events_spawn.await.unwrap(); -} diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index e9033c1cf..feb204237 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - [[package]] name = "abao" version = "0.2.0" @@ -60,19 +54,13 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] -[[package]] -name = "aliasable" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" - [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -189,39 +177,26 @@ dependencies = [ [[package]] name = "async-imap" -version = "0.7.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8379e2f1cdeb79afd2006932d7e8f64993fc0f7386d0ebc37231c90b05968c25" +checksum = "b538b767cbf9c162a6c5795d4b932bd2c20ba10b5a91a94d2b2b6886c1dce6a8" dependencies = [ "async-channel", - "async-native-tls 0.4.0", "base64 0.21.0", - "byte-pool", + "bytes", "chrono", "futures", "imap-proto", "log", "nom", "once_cell", - "ouroboros", "pin-utils", + "self_cell", "stop-token", "thiserror", "tokio", ] -[[package]] -name = "async-native-tls" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d57d4cec3c647232e1094dc013546c0b33ce785d8aeb251e1f20dfaf8a9a13fe" -dependencies = [ - "native-tls", - "thiserror", - "tokio", - "url", -] - [[package]] name = "async-native-tls" version = "0.5.0" @@ -362,9 +337,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.0.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "blake3" @@ -554,16 +529,6 @@ version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" -[[package]] -name = "byte-pool" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca" -dependencies = [ - "crossbeam-queue", - "stable_deref_trait", -] - [[package]] name = "bytemuck" version = "1.12.3" @@ -766,16 +731,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-queue" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.14" @@ -951,12 +906,12 @@ dependencies = [ [[package]] name = "deltachat" -version = "1.112.6" +version = "1.123.0" dependencies = [ "anyhow", "async-channel", "async-imap", - "async-native-tls 0.5.0", + "async-native-tls", "async-smtp", "async_zip", "backtrace", @@ -968,6 +923,7 @@ dependencies = [ "encoded-words", "escaper", "fast-socks5", + "fd-lock", "format-flowed", "futures", "futures-lite", @@ -979,7 +935,8 @@ dependencies = [ "lettre_email", "libc", "mailparse 0.14.0", - "num-derive", + "mime", + "num-derive 0.4.0", "num-traits", "num_cpus", "once_cell", @@ -1244,7 +1201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" dependencies = [ "serde", - "signature 1.6.4", + "signature 2.1.0", ] [[package]] @@ -1453,14 +1410,14 @@ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" [[package]] name = "enum-as-inner" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" dependencies = [ "heck", "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.15", ] [[package]] @@ -1488,6 +1445,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "errno-dragonfly" version = "0.1.2" @@ -1556,6 +1524,17 @@ dependencies = [ "instant", ] +[[package]] +name = "fd-lock" +version = "3.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" +dependencies = [ + "cfg-if", + "rustix 0.38.14", + "windows-sys 0.48.0", +] + [[package]] name = "ff" version = "0.12.1" @@ -1640,9 +1619,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -1701,9 +1680,9 @@ checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" [[package]] name = "futures-lite" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ "fastrand", "futures-core", @@ -1867,18 +1846,15 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -1975,7 +1951,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.7", "tokio", "tower-service", "tracing", @@ -2036,20 +2012,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.2.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "idna" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -2057,9 +2022,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" dependencies = [ "bytemuck", "byteorder", @@ -2124,10 +2089,10 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" dependencies = [ - "socket2", + "socket2 0.4.7", "widestring", "winapi", - "winreg", + "winreg 0.10.1", ] [[package]] @@ -2254,9 +2219,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.139" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libm" @@ -2303,6 +2268,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + [[package]] name = "lock_api" version = "0.4.9" @@ -2365,15 +2336,9 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "md-5" version = "0.10.5" @@ -2391,15 +2356,15 @@ checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" @@ -2418,14 +2383,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] @@ -2579,6 +2543,17 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "num-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2623,9 +2598,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", @@ -2651,9 +2626,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "opaque-debug" @@ -2663,9 +2638,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.48" +version = "0.10.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518915b97df115dd36109bfa429a48b8f737bd05508cf9588977b599648926d2" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" dependencies = [ "bitflags 1.3.2", "cfg-if", @@ -2704,11 +2679,10 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.83" +version = "0.9.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666416d899cf077260dac8698d60a60b435a46d57e82acb1be3d0dad87284e5b" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" dependencies = [ - "autocfg", "cc", "libc", "openssl-src", @@ -2716,29 +2690,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "ouroboros" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca" -dependencies = [ - "aliasable", - "ouroboros_macro", -] - -[[package]] -name = "ouroboros_macro" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d" -dependencies = [ - "Inflector", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.107", -] - [[package]] name = "overload" version = "0.1.1" @@ -2865,9 +2816,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pgp" @@ -2902,7 +2853,7 @@ dependencies = [ "md-5", "nom", "num-bigint-dig", - "num-derive", + "num-derive 0.3.3", "num-traits", "p256 0.13.1", "p384 0.13.0", @@ -2942,9 +2893,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -3135,9 +3086,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" -version = "0.28.2" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" dependencies = [ "memchr", ] @@ -3162,9 +3113,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4ced82a24bb281af338b9e8f94429b6eca01b4e66d899f40031f074e74c9" +checksum = "c956be1b23f4261676aed05a0046e204e8a6836e50203902683a718af0797989" dependencies = [ "bytes", "rand 0.8.5", @@ -3187,7 +3138,7 @@ checksum = "641538578b21f5e5c8ea733b736895576d0fe329bb883b937db6f4d163dbaaf4" dependencies = [ "libc", "quinn-proto", - "socket2", + "socket2 0.4.7", "tracing", "windows-sys 0.42.0", ] @@ -3316,13 +3267,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.0" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-automata 0.3.8", + "regex-syntax 0.7.5", ] [[package]] @@ -3331,7 +3283,18 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ - "regex-syntax", + "regex-syntax 0.6.28", +] + +[[package]] +name = "regex-automata" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", ] [[package]] @@ -3341,10 +3304,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] -name = "reqwest" -version = "0.11.16" +name = "regex-syntax" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "reqwest" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ "base64 0.21.0", "bytes", @@ -3374,7 +3343,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "winreg 0.50.0", ] [[package]] @@ -3480,7 +3449,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 2.0.2", + "bitflags 2.4.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -3531,13 +3500,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" dependencies = [ "bitflags 1.3.2", - "errno", + "errno 0.2.8", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.1.4", "windows-sys 0.42.0", ] +[[package]] +name = "rustix" +version = "0.38.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" +dependencies = [ + "bitflags 2.4.0", + "errno 0.3.3", + "libc", + "linux-raw-sys 0.4.7", + "windows-sys 0.48.0", +] + [[package]] name = "rustls" version = "0.20.8" @@ -3599,9 +3581,9 @@ dependencies = [ [[package]] name = "sanitize-filename" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c" +checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603" dependencies = [ "lazy_static", "regex", @@ -3690,6 +3672,12 @@ dependencies = [ "libc", ] +[[package]] +name = "self_cell" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" + [[package]] name = "semver" version = "1.0.17" @@ -3891,6 +3879,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.5.2" @@ -3955,12 +3953,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "stop-token" version = "0.7.0" @@ -3981,21 +3973,21 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" -version = "0.24.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" [[package]] name = "strum_macros" -version = "0.24.3" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", - "syn 1.0.107", + "syn 2.0.15", ] [[package]] @@ -4074,7 +4066,7 @@ dependencies = [ "cfg-if", "fastrand", "redox_syscall", - "rustix", + "rustix 0.36.7", "windows-sys 0.42.0", ] @@ -4183,22 +4175,21 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.25.0" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", - "memchr", "mio", "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.4", "tokio-macros", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] @@ -4213,13 +4204,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.15", ] [[package]] @@ -4249,9 +4240,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", @@ -4275,9 +4266,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes", "futures-core", @@ -4401,9 +4392,9 @@ dependencies = [ [[package]] name = "trust-dns-proto" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" +checksum = "0dc775440033cb114085f6f2437682b194fa7546466024b1037e82a48a052a69" dependencies = [ "async-trait", "cfg-if", @@ -4412,9 +4403,9 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "idna 0.2.3", + "idna", "ipnet", - "lazy_static", + "once_cell", "rand 0.8.5", "smallvec", "thiserror", @@ -4426,16 +4417,17 @@ dependencies = [ [[package]] name = "trust-dns-resolver" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" +checksum = "2dff7aed33ef3e8bf2c9966fccdfed93f93d46f432282ea875cd66faabc6ef2f" dependencies = [ "cfg-if", "futures-util", "ipconfig", - "lazy_static", "lru-cache", + "once_cell", "parking_lot", + "rand 0.8.5", "resolv-conf", "smallvec", "thiserror", @@ -4467,9 +4459,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" @@ -4516,12 +4508,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.3.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", - "idna 0.3.0", + "idna", "percent-encoding", ] @@ -4684,9 +4676,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.0" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f" dependencies = [ "ring", "untrusted", @@ -4767,21 +4759,51 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", + "windows_aarch64_gnullvm 0.42.0", "windows_aarch64_msvc 0.42.0", "windows_i686_gnu 0.42.0", "windows_i686_msvc 0.42.0", "windows_x86_64_gnu 0.42.0", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.42.0", "windows_x86_64_msvc 0.42.0", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_msvc" version = "0.32.0" @@ -4800,6 +4822,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_i686_gnu" version = "0.32.0" @@ -4818,6 +4846,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_msvc" version = "0.32.0" @@ -4836,6 +4870,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_x86_64_gnu" version = "0.32.0" @@ -4854,12 +4894,24 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_msvc" version = "0.32.0" @@ -4878,6 +4930,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "winreg" version = "0.10.1" @@ -4887,6 +4945,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "x25519-dalek" version = "2.0.0-pre.1" diff --git a/node/constants.js b/node/constants.js index 7752a3dd0..b9c990c77 100644 --- a/node/constants.js +++ b/node/constants.js @@ -48,6 +48,7 @@ module.exports = { DC_EVENT_LOCATION_CHANGED: 2035, DC_EVENT_MSGS_CHANGED: 2000, DC_EVENT_MSGS_NOTICED: 2008, + DC_EVENT_MSG_DELETED: 2016, DC_EVENT_MSG_DELIVERED: 2010, DC_EVENT_MSG_FAILED: 2012, DC_EVENT_MSG_READ: 2015, @@ -89,6 +90,7 @@ module.exports = { DC_KEY_GEN_DEFAULT: 0, DC_KEY_GEN_ED25519: 2, DC_KEY_GEN_RSA2048: 1, + DC_KEY_GEN_RSA4096: 3, DC_LP_AUTH_NORMAL: 4, DC_LP_AUTH_OAUTH2: 2, DC_MEDIA_QUALITY_BALANCED: 0, @@ -151,11 +153,14 @@ module.exports = { DC_STR_AEAP_EXPLANATION_AND_LINK: 123, DC_STR_ARCHIVEDCHATS: 40, DC_STR_AUDIO: 11, + DC_STR_BACKUP_TRANSFER_MSG_BODY: 163, DC_STR_BACKUP_TRANSFER_QR: 162, DC_STR_BAD_TIME_MSG_BODY: 85, DC_STR_BROADCAST_LIST: 115, DC_STR_CANNOT_LOGIN: 60, DC_STR_CANTDECRYPT_MSG_BODY: 29, + DC_STR_CHAT_PROTECTION_DISABLED: 171, + DC_STR_CHAT_PROTECTION_ENABLED: 170, DC_STR_CONFIGURATION_FAILED: 84, DC_STR_CONNECTED: 107, DC_STR_CONNTECTING: 108, @@ -241,12 +246,6 @@ module.exports = { DC_STR_OUTGOING_MESSAGES: 104, DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99, DC_STR_PART_OF_TOTAL_USED: 116, - DC_STR_PROTECTION_DISABLED: 89, - DC_STR_PROTECTION_DISABLED_BY_OTHER: 161, - DC_STR_PROTECTION_DISABLED_BY_YOU: 160, - DC_STR_PROTECTION_ENABLED: 88, - DC_STR_PROTECTION_ENABLED_BY_OTHER: 159, - DC_STR_PROTECTION_ENABLED_BY_YOU: 158, DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98, DC_STR_READRCPT: 31, DC_STR_READRCPT_MAILBODY: 32, diff --git a/node/events.js b/node/events.js index d27fd1829..5a24586a2 100644 --- a/node/events.js +++ b/node/events.js @@ -22,6 +22,7 @@ module.exports = { 2010: 'DC_EVENT_MSG_DELIVERED', 2012: 'DC_EVENT_MSG_FAILED', 2015: 'DC_EVENT_MSG_READ', + 2016: 'DC_EVENT_MSG_DELETED', 2020: 'DC_EVENT_CHAT_MODIFIED', 2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED', 2030: 'DC_EVENT_CONTACTS_CHANGED', diff --git a/node/lib/constants.ts b/node/lib/constants.ts index 2452ce754..6cc08f144 100644 --- a/node/lib/constants.ts +++ b/node/lib/constants.ts @@ -48,6 +48,7 @@ export enum C { DC_EVENT_LOCATION_CHANGED = 2035, DC_EVENT_MSGS_CHANGED = 2000, DC_EVENT_MSGS_NOTICED = 2008, + DC_EVENT_MSG_DELETED = 2016, DC_EVENT_MSG_DELIVERED = 2010, DC_EVENT_MSG_FAILED = 2012, DC_EVENT_MSG_READ = 2015, @@ -89,6 +90,7 @@ export enum C { DC_KEY_GEN_DEFAULT = 0, DC_KEY_GEN_ED25519 = 2, DC_KEY_GEN_RSA2048 = 1, + DC_KEY_GEN_RSA4096 = 3, DC_LP_AUTH_NORMAL = 4, DC_LP_AUTH_OAUTH2 = 2, DC_MEDIA_QUALITY_BALANCED = 0, @@ -151,11 +153,14 @@ export enum C { DC_STR_AEAP_EXPLANATION_AND_LINK = 123, DC_STR_ARCHIVEDCHATS = 40, DC_STR_AUDIO = 11, + DC_STR_BACKUP_TRANSFER_MSG_BODY = 163, DC_STR_BACKUP_TRANSFER_QR = 162, DC_STR_BAD_TIME_MSG_BODY = 85, DC_STR_BROADCAST_LIST = 115, DC_STR_CANNOT_LOGIN = 60, DC_STR_CANTDECRYPT_MSG_BODY = 29, + DC_STR_CHAT_PROTECTION_DISABLED = 171, + DC_STR_CHAT_PROTECTION_ENABLED = 170, DC_STR_CONFIGURATION_FAILED = 84, DC_STR_CONNECTED = 107, DC_STR_CONNTECTING = 108, @@ -241,12 +246,6 @@ export enum C { DC_STR_OUTGOING_MESSAGES = 104, DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99, DC_STR_PART_OF_TOTAL_USED = 116, - DC_STR_PROTECTION_DISABLED = 89, - DC_STR_PROTECTION_DISABLED_BY_OTHER = 161, - DC_STR_PROTECTION_DISABLED_BY_YOU = 160, - DC_STR_PROTECTION_ENABLED = 88, - DC_STR_PROTECTION_ENABLED_BY_OTHER = 159, - DC_STR_PROTECTION_ENABLED_BY_YOU = 158, DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98, DC_STR_READRCPT = 31, DC_STR_READRCPT_MAILBODY = 32, @@ -306,6 +305,7 @@ export const EventId2EventName: { [key: number]: string } = { 2010: 'DC_EVENT_MSG_DELIVERED', 2012: 'DC_EVENT_MSG_FAILED', 2015: 'DC_EVENT_MSG_READ', + 2016: 'DC_EVENT_MSG_DELETED', 2020: 'DC_EVENT_CHAT_MODIFIED', 2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED', 2030: 'DC_EVENT_CONTACTS_CHANGED', diff --git a/node/lib/context.ts b/node/lib/context.ts index 4c926ba78..b292e9e35 100644 --- a/node/lib/context.ts +++ b/node/lib/context.ts @@ -36,7 +36,7 @@ export class Context extends EventEmitter { } } - /** Opens a stanalone context (without an account manager) + /** Opens a standalone context (without an account manager) * automatically starts the event handler */ static open(cwd: string): Context { const dbFile = join(cwd, 'db.sqlite') @@ -699,23 +699,6 @@ export class Context extends EventEmitter { ) } - /** - * - * @param chatId - * @param protect - * @returns success boolean - */ - setChatProtection(chatId: number, protect: boolean) { - debug(`setChatProtection ${chatId} ${protect}`) - return Boolean( - binding.dcn_set_chat_protection( - this.dcn_context, - Number(chatId), - protect ? 1 : 0 - ) - ) - } - getChatEphemeralTimer(chatId: number): number { debug(`getChatEphemeralTimer ${chatId}`) return binding.dcn_get_chat_ephemeral_timer( diff --git a/node/lib/deltachat.ts b/node/lib/deltachat.ts index 2526b9378..dbf002e46 100644 --- a/node/lib/deltachat.ts +++ b/node/lib/deltachat.ts @@ -21,12 +21,15 @@ export class AccountManager extends EventEmitter { accountDir: string jsonRpcStarted = false - constructor(cwd: string, os = 'deltachat-node') { + constructor(cwd: string, writable = true) { super() debug('DeltaChat constructor') this.accountDir = cwd - this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir) + this.dcn_accounts = binding.dcn_accounts_new( + this.accountDir, + writable ? 1 : 0 + ) } getAllAccountIds() { diff --git a/node/src/module.c b/node/src/module.c index 3d3045017..5c675020e 100644 --- a/node/src/module.c +++ b/node/src/module.c @@ -1399,18 +1399,6 @@ NAPI_METHOD(dcn_set_chat_name) { NAPI_RETURN_INT32(result); } -NAPI_METHOD(dcn_set_chat_protection) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - NAPI_ARGV_INT32(protect, 1); - - int result = dc_set_chat_protection(dcn_context->dc_context, - chat_id, - protect); - NAPI_RETURN_INT32(result); -} - NAPI_METHOD(dcn_get_chat_ephemeral_timer) { NAPI_ARGV(2); NAPI_DCN_CONTEXT(); @@ -2915,8 +2903,8 @@ NAPI_METHOD(dcn_msg_get_webxdc_blob){ NAPI_METHOD(dcn_accounts_new) { NAPI_ARGV(2); - NAPI_ARGV_UTF8_MALLOC(os_name, 0); - NAPI_ARGV_UTF8_MALLOC(dir, 1); + NAPI_ARGV_UTF8_MALLOC(dir, 0); + NAPI_ARGV_INT32(writable, 1); TRACE("calling.."); dcn_accounts_t* dcn_accounts = calloc(1, sizeof(dcn_accounts_t)); @@ -2925,7 +2913,7 @@ NAPI_METHOD(dcn_accounts_new) { } - dcn_accounts->dc_accounts = dc_accounts_new(os_name, dir); + dcn_accounts->dc_accounts = dc_accounts_new(dir, writable); napi_value result; NAPI_STATUS_THROWS(napi_create_external(env, dcn_accounts, @@ -3491,7 +3479,6 @@ NAPI_INIT() { NAPI_EXPORT_FUNCTION(dcn_send_msg); NAPI_EXPORT_FUNCTION(dcn_send_videochat_invitation); NAPI_EXPORT_FUNCTION(dcn_set_chat_name); - NAPI_EXPORT_FUNCTION(dcn_set_chat_protection); NAPI_EXPORT_FUNCTION(dcn_get_chat_ephemeral_timer); NAPI_EXPORT_FUNCTION(dcn_set_chat_ephemeral_timer); NAPI_EXPORT_FUNCTION(dcn_set_chat_profile_image); diff --git a/node/test/test.js b/node/test/test.js index 3c1baed01..2c7912584 100644 --- a/node/test/test.js +++ b/node/test/test.js @@ -446,7 +446,8 @@ describe('Offline Tests with unconfigured account', function () { context.setChatProfileImage(chatId, imagePath) const blobPath = context.getChat(chatId).getProfileImage() expect(blobPath.startsWith(blobs)).to.be.true - expect(blobPath.endsWith(image)).to.be.true + expect(blobPath.includes('image')).to.be.true + expect(blobPath.endsWith('.jpeg')).to.be.true context.setChatProfileImage(chatId, null) expect(context.getChat(chatId).getProfileImage()).to.be.equal( diff --git a/package.json b/package.json index 6716b8c01..f3093f89c 100644 --- a/package.json +++ b/package.json @@ -60,5 +60,5 @@ "test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit" }, "types": "node/dist/index.d.ts", - "version": "1.112.6" + "version": "1.126.1" } diff --git a/python/LICENSE b/python/LICENSE index 14e2f777f..ee6256cdb 100644 --- a/python/LICENSE +++ b/python/LICENSE @@ -357,7 +357,7 @@ Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. + file, You can obtain one at https://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE diff --git a/python/README.rst b/python/README.rst index da1b25c7e..c5f048912 100644 --- a/python/README.rst +++ b/python/README.rst @@ -74,7 +74,9 @@ Developing the bindings If you want to develop or debug the bindings, you can create a testing development environment using `tox`:: - tox -c python --devenv env + export DCC_RS_DEV="$PWD" + export DCC_RS_TARGET=debug + tox -c python --devenv env -e py . env/bin/activate Inside this environment the bindings are installed diff --git a/python/doc/api.rst b/python/doc/api.rst index 049bc2928..f19306391 100644 --- a/python/doc/api.rst +++ b/python/doc/api.rst @@ -2,34 +2,34 @@ high level API reference ======================== -- :class:`deltachat.account.Account` (your main entry point, creates the +- :class:`deltachat.Account` (your main entry point, creates the other classes) -- :class:`deltachat.contact.Contact` -- :class:`deltachat.chat.Chat` -- :class:`deltachat.message.Message` +- :class:`deltachat.Contact` +- :class:`deltachat.Chat` +- :class:`deltachat.Message` Account ------- -.. autoclass:: deltachat.account.Account +.. autoclass:: deltachat.Account :members: Contact ------- -.. autoclass:: deltachat.contact.Contact +.. autoclass:: deltachat.Contact :members: Chat ---- -.. autoclass:: deltachat.chat.Chat +.. autoclass:: deltachat.Chat :members: Message ------- -.. autoclass:: deltachat.message.Message +.. autoclass:: deltachat.Message :members: diff --git a/python/examples/group_tracking.py b/python/examples/group_tracking.py index 7ca003c0d..51f63cb74 100644 --- a/python/examples/group_tracking.py +++ b/python/examples/group_tracking.py @@ -32,25 +32,13 @@ class GroupTrackingPlugin: @account_hookimpl def ac_member_added(self, chat, contact, actor, message): - print( - "ac_member_added {} to chat {} from {}".format( - contact.addr, - chat.id, - actor or message.get_sender_contact().addr, - ), - ) + print(f"ac_member_added {contact.addr} to chat {chat.id} from {actor or message.get_sender_contact().addr}") for member in chat.get_contacts(): print(f"chat member: {member.addr}") @account_hookimpl def ac_member_removed(self, chat, contact, actor, message): - print( - "ac_member_removed {} from chat {} by {}".format( - contact.addr, - chat.id, - actor or message.get_sender_contact().addr, - ), - ) + print(f"ac_member_removed {contact.addr} from chat {chat.id} by {actor or message.get_sender_contact().addr}") def main(argv=None): diff --git a/python/examples/test_examples.py b/python/examples/test_examples.py index 6e4f6ef25..ca8e48b69 100644 --- a/python/examples/test_examples.py +++ b/python/examples/test_examples.py @@ -24,8 +24,6 @@ def test_echo_quit_plugin(acfactory, lp): lp.sec("creating a temp account to contact the bot") (ac1,) = acfactory.get_online_accounts(1) - botproc.await_resync() - lp.sec("sending a message to the bot") bot_contact = ac1.create_contact(botproc.addr) bot_chat = bot_contact.create_chat() @@ -54,8 +52,6 @@ def test_group_tracking_plugin(acfactory, lp): ac1.add_account_plugin(FFIEventLogger(ac1)) ac2.add_account_plugin(FFIEventLogger(ac2)) - botproc.await_resync() - lp.sec("creating bot test group with bot") bot_contact = ac1.create_contact(botproc.addr) ch = ac1.create_group_chat("bot test group") diff --git a/python/mypy.ini b/python/mypy.ini index 6b7560ab9..3eb9ae8c8 100644 --- a/python/mypy.ini +++ b/python/mypy.ini @@ -24,3 +24,5 @@ ignore_missing_imports = True [mypy-imap_tools.*] ignore_missing_imports = True +[mypy-distutils.*] +ignore_missing_imports = True diff --git a/python/pyproject.toml b/python/pyproject.toml index fb846cc8f..5e0bee124 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -11,10 +11,11 @@ authors = [ { name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" }, ] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", "Programming Language :: Python :: 3", + "Topic :: Communications :: Chat", "Topic :: Communications :: Email", "Topic :: Software Development :: Libraries", ] @@ -33,6 +34,7 @@ dynamic = [ "Home" = "https://github.com/deltachat/deltachat-core-rust/" "Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues" "Documentation" = "https://py.delta.chat/" +"Mastodon" = "https://chaos.social/@delta" [project.entry-points.pytest11] "deltachat.testplugin" = "deltachat.testplugin" diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py index 6d067f4fb..69268e18c 100644 --- a/python/src/deltachat/account.py +++ b/python/src/deltachat/account.py @@ -6,7 +6,7 @@ from array import array from contextlib import contextmanager from email.utils import parseaddr from threading import Event -from typing import Any, Dict, Generator, List, Optional, Union, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Union from . import const, hookspec from .capi import ffi, lib @@ -195,7 +195,7 @@ class Account: assert res != ffi.NULL, f"config value not found for: {name!r}" return from_dc_charpointer(res) - def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None: + def _preconfigure_keypair(self, addr: str, secret: str) -> None: """See dc_preconfigure_keypair() in deltachat.h. In other words, you don't need this. @@ -203,7 +203,7 @@ class Account: res = lib.dc_preconfigure_keypair( self._dc_context, as_dc_charpointer(addr), - as_dc_charpointer(public), + ffi.NULL, as_dc_charpointer(secret), ) if res == 0: @@ -427,7 +427,7 @@ class Account: assert dc_chatlist != ffi.NULL chatlist = [] - for i in range(0, lib.dc_chatlist_get_cnt(dc_chatlist)): + for i in range(lib.dc_chatlist_get_cnt(dc_chatlist)): chat_id = lib.dc_chatlist_get_chat_id(dc_chatlist, i) chatlist.append(Chat(self, chat_id)) return chatlist @@ -617,18 +617,18 @@ class Account: # meta API for start/stop and event based processing # - def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False): - from .events import FFIEventLogger - + def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False, displayname=None): """get the account running, configure it if necessary. add plugins if provided. :param addr: the email address of the account :param password: the password of the account :param account_plugins: a list of plugins to add :param show_ffi: show low level ffi events + :param displayname: the display name of the account """ + from .events import FFIEventLogger + if show_ffi: - self.set_config("displayname", "bot") log = FFIEventLogger(self) self.add_account_plugin(log) @@ -644,6 +644,8 @@ class Account: configtracker = self.configure() configtracker.wait_finish() + if displayname: + self.set_config("displayname", displayname) # start IO threads and configure if necessary self.start_io() diff --git a/python/src/deltachat/contact.py b/python/src/deltachat/contact.py index f49b91fbc..e28a31178 100644 --- a/python/src/deltachat/contact.py +++ b/python/src/deltachat/contact.py @@ -71,13 +71,16 @@ class Contact: """Unblock this contact. Messages from this contact will be retrieved (again).""" return lib.dc_block_contact(self.account._dc_context, self.id, False) - def is_verified(self): + def is_verified(self) -> bool: """Return True if the contact is verified.""" - return lib.dc_contact_is_verified(self._dc_contact) + return lib.dc_contact_is_verified(self._dc_contact) == 2 - def get_verifier(self, contact): + def get_verifier(self, contact) -> Optional["Contact"]: """Return the address of the contact that verified the contact.""" - return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact)) + verifier_id = lib.dc_contact_get_verifier_id(contact._dc_contact) + if verifier_id == 0: + return None + return Contact(self.account, verifier_id) def get_profile_image(self) -> Optional[str]: """Get contact profile image. diff --git a/python/src/deltachat/cutil.py b/python/src/deltachat/cutil.py index ad9810219..f907166b9 100644 --- a/python/src/deltachat/cutil.py +++ b/python/src/deltachat/cutil.py @@ -15,7 +15,7 @@ def as_dc_charpointer(obj): def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]: - for i in range(0, lib.dc_array_get_cnt(dc_array_t)): + for i in range(lib.dc_array_get_cnt(dc_array_t)): yield constructor(lib.dc_array_get_id(dc_array_t, i)) diff --git a/python/src/deltachat/events.py b/python/src/deltachat/events.py index 0ae86a9ad..4c4ec5119 100644 --- a/python/src/deltachat/events.py +++ b/python/src/deltachat/events.py @@ -9,11 +9,11 @@ from contextlib import contextmanager from queue import Empty, Queue from . import const +from .account import Account from .capi import ffi, lib from .cutil import from_optional_dc_charpointer from .hookspec import account_hookimpl from .message import map_system_message -from .account import Account def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}): diff --git a/python/src/deltachat/message.py b/python/src/deltachat/message.py index e58d97d05..3f813e52d 100644 --- a/python/src/deltachat/message.py +++ b/python/src/deltachat/message.py @@ -486,6 +486,9 @@ class Message: dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref) return lib.dc_msg_get_download_state(dc_msg) + def download_full(self) -> None: + lib.dc_download_full_msg(self.account._dc_context, self.id) + # some code for handling DC_MSG_* view types @@ -507,8 +510,7 @@ def get_viewtype_code_from_name(view_type_name): if code is not None: return code raise ValueError( - "message typecode not found for {!r}, " - "available {!r}".format(view_type_name, list(_view_type_mapping.keys())), + f"message typecode not found for {view_type_name!r}, available {list(_view_type_mapping.keys())!r}", ) diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py index 192afb508..944982475 100644 --- a/python/src/deltachat/testplugin.py +++ b/python/src/deltachat/testplugin.py @@ -9,7 +9,7 @@ import threading import time import weakref from queue import Queue -from typing import Callable, List, Optional, Dict, Set +from typing import Callable, Dict, List, Optional, Set import pytest import requests @@ -137,6 +137,9 @@ def pytest_report_header(config, startdir): @pytest.fixture(scope="session") def testprocess(request): + """Return live account configuration manager. + + The returned object is a :class:`TestProcess` object.""" return TestProcess(pytestconfig=request.config) @@ -231,6 +234,8 @@ def write_dict_to_dir(dic, target_dir): @pytest.fixture() def data(request): + """Test data.""" + class Data: def __init__(self) -> None: # trying to find test data heuristically @@ -473,10 +478,9 @@ class ACFactory: except IndexError: pass else: - fname_pub = self.data.read_path(f"key/{keyname}-public.asc") fname_sec = self.data.read_path(f"key/{keyname}-secret.asc") - if fname_pub and fname_sec: - account._preconfigure_keypair(addr, fname_pub, fname_sec) + if fname_sec: + account._preconfigure_keypair(addr, fname_sec) return True print(f"WARN: could not use preconfigured keys for {addr!r}") @@ -614,6 +618,7 @@ class ACFactory: @pytest.fixture() def acfactory(request, tmpdir, testprocess, data): + """Account factory.""" am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data) yield am if hasattr(request.node, "rep_call") and request.node.rep_call.failed: @@ -676,21 +681,17 @@ class BotProcess: print("+++IGN:", line) ignored.append(line) - def await_resync(self): - self.fnmatch_lines( - """ - *Resync: collected * message IDs in folder INBOX* - """, - ) - @pytest.fixture() def tmp_db_path(tmpdir): + """Return a path inside the temporary directory where the database can be created.""" return tmpdir.join("test.db").strpath @pytest.fixture() def lp(): + """Log printer fixture.""" + class Printer: def sec(self, msg: str) -> None: print() diff --git a/python/src/deltachat/tracker.py b/python/src/deltachat/tracker.py index f8fbc3f8f..1fa88044c 100644 --- a/python/src/deltachat/tracker.py +++ b/python/src/deltachat/tracker.py @@ -1,6 +1,6 @@ from queue import Queue from threading import Event -from typing import List, TYPE_CHECKING +from typing import TYPE_CHECKING, List from .hookspec import Global, account_hookimpl diff --git a/python/tests/bench_test_setup.py b/python/tests/bench_test_setup.py index 5dc5e3877..ef750550f 100644 --- a/python/tests/bench_test_setup.py +++ b/python/tests/bench_test_setup.py @@ -15,6 +15,6 @@ class TestEmpty: def test_prepare_setup_measurings(self, acfactory): acfactory.get_online_accounts(BENCH_NUM) - @pytest.mark.parametrize("num", range(0, BENCH_NUM + 1)) + @pytest.mark.parametrize("num", range(BENCH_NUM + 1)) def test_setup_online_accounts(self, acfactory, num): acfactory.get_online_accounts(num) diff --git a/python/tests/stress_test_db.py b/python/tests/stress_test_db.py index 43a04116a..9a793a816 100644 --- a/python/tests/stress_test_db.py +++ b/python/tests/stress_test_db.py @@ -8,7 +8,7 @@ import pytest import deltachat -def test_db_busy_error(acfactory, tmpdir): +def test_db_busy_error(acfactory): starttime = time.time() log_lock = threading.RLock() diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index 736c0d1dd..75c554a6d 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -1,6 +1,7 @@ import sys import pytest +import deltachat as dc class TestGroupStressTests: @@ -136,6 +137,7 @@ def test_qr_verified_group_and_chatting(acfactory, lp): lp.sec("ac2: read member added message") msg = ac2._evtracker.wait_next_incoming_message() assert msg.is_encrypted() + assert msg.is_system_message() assert "added" in msg.text.lower() lp.sec("ac1: send message") @@ -149,9 +151,8 @@ def test_qr_verified_group_and_chatting(acfactory, lp): assert msg.is_encrypted() lp.sec("ac2: Check that ac2 verified ac1") - # If we verified the contact ourselves then verifier addr == contact addr ac2_ac1_contact = ac2.get_contacts()[0] - assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr + assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF lp.sec("ac2: send message and let ac1 read it") chat2.send_text("world") @@ -176,14 +177,15 @@ def test_qr_verified_group_and_chatting(acfactory, lp): lp.sec("ac2: Check that ac1 verified ac3 for ac2") ac2_ac1_contact = ac2.get_contacts()[0] - assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr + assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF ac2_ac3_contact = ac2.get_contacts()[1] - assert ac2.get_self_contact().get_verifier(ac2_ac3_contact) == ac1_addr + assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr lp.sec("ac2: send message and let ac3 read it") chat2.send_text("hi") - # Skip system message about added member - ac3._evtracker.wait_next_incoming_message() + # System message about the added member. + msg = ac3._evtracker.wait_next_incoming_message() + assert msg.is_system_message() msg = ac3._evtracker.wait_next_incoming_message() assert msg.text == "hi" assert msg.is_encrypted() @@ -494,7 +496,7 @@ def test_multidevice_sync_seen(acfactory, lp): assert "Expires: " in ac1_clone_message.get_message_info() -def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp): +def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp): """The test for the bug #3836: - Alice has two devices, the second is offline. - Alice creates a verified group and sends a QR invitation to Bob. @@ -507,9 +509,10 @@ def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp): for ac in [ac1, ac1_offl]: ac.set_config("bcc_self", "1") acfactory.bring_accounts_online() - dir = tmpdir.mkdir("exportdir") - ac1.export_self_keys(dir.strpath) - ac1_offl.import_self_keys(dir.strpath) + dir = tmp_path / "exportdir" + dir.mkdir() + ac1.export_self_keys(str(dir)) + ac1_offl.import_self_keys(str(dir)) ac1_offl.stop_io() lp.sec("ac1: create verified-group QR, ac2 scans and joins") @@ -523,7 +526,8 @@ def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp): lp.sec("ac2: sending message") # Message can be sent only after a receipt of "vg-member-added" message. Just wait for # "Member Me () added by ." message. - ac2._evtracker.wait_next_incoming_message() + msg_in = ac2._evtracker.wait_next_incoming_message() + assert msg_in.is_system_message() msg_out = chat2.send_text("hello") lp.sec("ac1: receiving message") @@ -538,10 +542,8 @@ def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp): assert msg_in.text == msg_out.text assert msg_in.get_sender_contact().addr == ac2_addr - ac1.set_config("bcc_self", "0") - -def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp): +def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp): """Another test for the bug #3836: - Bob has two devices, the second is offline. - Alice creates a verified group and sends a QR invitation to Bob. @@ -556,9 +558,10 @@ def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp): for ac in [ac2, ac2_offl]: ac.set_config("bcc_self", "1") acfactory.bring_accounts_online() - dir = tmpdir.mkdir("exportdir") - ac2.export_self_keys(dir.strpath) - ac2_offl.import_self_keys(dir.strpath) + dir = tmp_path / "exportdir" + dir.mkdir() + ac2.export_self_keys(str(dir)) + ac2_offl.import_self_keys(str(dir)) ac2_offl.stop_io() lp.sec("ac1: create verified-group QR, ac2 scans and joins") @@ -587,4 +590,67 @@ def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp): assert msg_in.get_sender_contact().addr == ac2.get_config("addr") assert msg_in.text == msg_out.text - ac2.set_config("bcc_self", "0") + +def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp): + """Test for the issue #4346: + - User is added to a verified group. + - First device of the user downloads "member added" from the group. + - First device removes "member added" from the server. + - Some new messages are sent to the group. + - Second device comes online, receives these new messages. The result is a verified group with unverified members. + - First device re-gossips Autocrypt keys to the group. + - Now the seconds device has all members verified. + """ + ac1, ac2 = acfactory.get_online_accounts(2) + ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2) + for ac in [ac2, ac2_offl]: + ac.set_config("bcc_self", "1") + ac2.set_config("delete_server_after", "1") + ac2.set_config("gossip_period", "0") # Re-gossip in every message + acfactory.bring_accounts_online() + dir = tmp_path / "exportdir" + dir.mkdir() + ac2.export_self_keys(str(dir)) + ac2_offl.import_self_keys(str(dir)) + ac2_offl.stop_io() + + lp.sec("ac1: create verified-group QR, ac2 scans and joins") + chat1 = ac1.create_group_chat("hello", verified=True) + assert chat1.is_protected() + qr = chat1.get_join_qr() + lp.sec("ac2: start QR-code based join-group protocol") + chat2 = ac2.qr_join_chat(qr) + ac1._evtracker.wait_securejoin_inviter_progress(1000) + # Wait for "Member Me () added by ." message. + msg_in = ac2._evtracker.wait_next_incoming_message() + assert msg_in.is_system_message() + + lp.sec("ac2: waiting for 'member added' to be deleted on the server") + ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED") + + lp.sec("ac1: sending 'hi' to the group") + ac2.set_config("delete_server_after", "0") + chat1.send_text("hi") + + lp.sec("ac2_offl: going online, checking the 'hi' message") + ac2_offl.start_io() + msg_in = ac2_offl._evtracker.wait_next_incoming_message() + assert not msg_in.is_system_message() + assert msg_in.text.startswith("[The message was sent with non-verified encryption") + ac2_offl_ac1_contact = msg_in.get_sender_contact() + assert ac2_offl_ac1_contact.addr == ac1.get_config("addr") + assert not ac2_offl_ac1_contact.is_verified() + chat2_offl = msg_in.chat + assert chat2_offl.is_protected() + + lp.sec("ac2: sending message re-gossiping Autocrypt keys") + chat2.send_text("hi2") + + lp.sec("ac2_offl: receiving message") + ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") + msg_in = ac2_offl.get_message_by_id(ev.data2) + assert not msg_in.is_system_message() + assert msg_in.text == "hi2" + assert msg_in.chat == chat2_offl + assert msg_in.get_sender_contact().addr == ac2.get_config("addr") + assert ac2_offl_ac1_contact.is_verified() diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 85e1774bd..431463952 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -2,18 +2,18 @@ import os import queue import sys import time +import base64 from datetime import datetime, timezone import pytest from imap_tools import AND, U -from deltachat import const -from deltachat.hookspec import account_hookimpl -from deltachat.message import Message +import deltachat as dc +from deltachat import account_hookimpl, Message, Chat from deltachat.tracker import ImexTracker -def test_basic_imap_api(acfactory, tmpdir): +def test_basic_imap_api(acfactory, tmp_path): ac1, ac2 = acfactory.get_online_accounts(2) chat12 = acfactory.get_accepted_chat(ac1, ac2) @@ -28,7 +28,7 @@ def test_basic_imap_api(acfactory, tmpdir): imap2.mark_all_read() assert imap2.get_unread_cnt() == 0 - imap2.dump_imap_structures(tmpdir, logfile=sys.stdout) + imap2.dump_imap_structures(tmp_path, logfile=sys.stdout) imap2.shutdown() @@ -36,8 +36,8 @@ def test_basic_imap_api(acfactory, tmpdir): def test_configure_generate_key(acfactory, lp): # A slow test which will generate new keys. acfactory.remove_preconfigured_keys() - ac1 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_RSA2048)) - ac2 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_ED25519)) + ac1 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_RSA2048)) + ac2 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_ED25519)) acfactory.bring_accounts_online() chat = acfactory.get_accepted_chat(ac1, ac2) @@ -72,35 +72,37 @@ def test_configure_canceled(acfactory): pass -def test_configure_unref(tmpdir): +def test_configure_unref(tmp_path): """Test that removing the last reference to the context during ongoing configuration does not result in use-after-free.""" from deltachat.capi import ffi, lib - path = tmpdir.mkdir("test_configure_unref").join("dc.db").strpath - dc_context = lib.dc_context_new(ffi.NULL, path.encode("utf8"), ffi.NULL) + path = tmp_path / "test_configure_unref" + path.mkdir() + dc_context = lib.dc_context_new(ffi.NULL, str(path / "dc.db").encode("utf8"), ffi.NULL) lib.dc_set_config(dc_context, "addr".encode("utf8"), "foo@x.testrun.org".encode("utf8")) lib.dc_set_config(dc_context, "mail_pw".encode("utf8"), "abc".encode("utf8")) lib.dc_configure(dc_context) lib.dc_context_unref(dc_context) -def test_export_import_self_keys(acfactory, tmpdir, lp): +def test_export_import_self_keys(acfactory, tmp_path, lp): ac1, ac2 = acfactory.get_online_accounts(2) - dir = tmpdir.mkdir("exportdir") - export_files = ac1.export_self_keys(dir.strpath) + dir = tmp_path / "exportdir" + dir.mkdir() + export_files = ac1.export_self_keys(str(dir)) assert len(export_files) == 2 for x in export_files: - assert x.startswith(dir.strpath) + assert x.startswith(str(dir)) (key_id,) = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*") ac1._evtracker.consume_events() lp.sec("exported keys (private and public)") - for name in os.listdir(dir.strpath): - lp.indent(dir.strpath + os.sep + name) + for name in dir.iterdir(): + lp.indent(str(dir / name)) lp.sec("importing into existing account") - ac2.import_self_keys(dir.strpath) + ac2.import_self_keys(str(dir)) (key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*") assert key_id2 == key_id @@ -156,62 +158,65 @@ def test_one_account_send_bcc_setting(acfactory, lp): assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1 -def test_send_file_twice_unicode_filename_mangling(tmpdir, acfactory, lp): +def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp): ac1, ac2 = acfactory.get_online_accounts(2) chat = acfactory.get_accepted_chat(ac1, ac2) - basename = "somedäüta.html.zip" - p = os.path.join(tmpdir.strpath, basename) - with open(p, "w") as f: - f.write("some data") + basename = "somedäüta" + ext = ".html.zip" + p = tmp_path / (basename + ext) + p.write_text("some data") def send_and_receive_message(): lp.sec("ac1: prepare and send attachment + text to ac2") msg1 = Message.new_empty(ac1, "file") msg1.set_text("withfile") - msg1.set_file(p) + msg1.set_file(str(p)) chat.send_msg(msg1) lp.sec("ac2: receive message") ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG") - assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL + assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL return ac2.get_message_by_id(ev.data2) msg = send_and_receive_message() assert msg.text == "withfile" assert open(msg.filename).read() == "some data" - assert msg.filename.endswith(basename) + msg.filename.index(basename) + assert msg.filename.endswith(ext) msg2 = send_and_receive_message() assert msg2.text == "withfile" assert open(msg2.filename).read() == "some data" - assert msg2.filename.endswith("html.zip") + msg2.filename.index(basename) + assert msg2.filename.endswith(ext) assert msg.filename != msg2.filename -def test_send_file_html_attachment(tmpdir, acfactory, lp): +def test_send_file_html_attachment(tmp_path, acfactory, lp): ac1, ac2 = acfactory.get_online_accounts(2) chat = acfactory.get_accepted_chat(ac1, ac2) - basename = "test.html" + basename = "test" + ext = ".html" content = "textdata" - p = os.path.join(tmpdir.strpath, basename) - with open(p, "w") as f: - # write wrong html to see if core tries to parse it - # (it shouldn't as it's a file attachment) - f.write(content) + p = tmp_path / (basename + ext) + # write wrong html to see if core tries to parse it + # (it shouldn't as it's a file attachment) + p.write_text(content) lp.sec("ac1: prepare and send attachment + text to ac2") - chat.send_file(p, mime_type="text/html") + chat.send_file(str(p), mime_type="text/html") lp.sec("ac2: receive message") ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG") - assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL + assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL msg = ac2.get_message_by_id(ev.data2) assert open(msg.filename).read() == content - assert msg.filename.endswith(basename) + msg.filename.index(basename) + assert msg.filename.endswith(ext) def test_html_message(acfactory, lp): @@ -324,6 +329,59 @@ def test_webxdc_message(acfactory, data, lp): assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1 +def test_webxdc_huge_update(acfactory, data, lp): + ac1, ac2 = acfactory.get_online_accounts(2) + chat = ac1.create_chat(ac2) + + msg1 = Message.new_empty(ac1, "webxdc") + msg1.set_text("message1") + msg1.set_file(data.get_path("webxdc/minimal.xdc")) + msg1 = chat.send_msg(msg1) + assert msg1.is_webxdc() + assert msg1.filename + + msg2 = ac2._evtracker.wait_next_incoming_message() + assert msg2.is_webxdc() + + payload = "A" * 1000 + assert msg1.send_status_update({"payload": payload}, "some test data") + ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE") + update = msg2.get_status_updates()[0] + assert update["payload"] == payload + + +def test_webxdc_download_on_demand(acfactory, data, lp): + ac1, ac2 = acfactory.get_online_accounts(2) + acfactory.introduce_each_other([ac1, ac2]) + chat = acfactory.get_accepted_chat(ac1, ac2) + + msg1 = Message.new_empty(ac1, "webxdc") + msg1.set_text("message1") + msg1.set_file(data.get_path("webxdc/minimal.xdc")) + msg1 = chat.send_msg(msg1) + assert msg1.is_webxdc() + assert msg1.filename + + msg2 = ac2._evtracker.wait_next_incoming_message() + assert msg2.is_webxdc() + + lp.sec("ac2 sets download limit") + ac2.set_config("download_limit", "100") + assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(50000))}, "some test data") + ac2_update = ac2._evtracker.wait_next_incoming_message() + assert ac2_update.download_state == dc.const.DC_DOWNLOAD_AVAILABLE + assert not msg2.get_status_updates() + + ac2_update.download_full() + ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE") + assert msg2.get_status_updates() + + # Get a event notifying that the message disappeared from the chat. + msgs_changed_event = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED") + assert msgs_changed_event.data1 == msg2.chat.id + assert msgs_changed_event.data2 == 0 + + def test_mvbox_sentbox_threads(acfactory, lp): lp.sec("ac1: start with mvbox thread") ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True) @@ -351,7 +409,42 @@ def test_move_works(acfactory): # Message is downloaded ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG") - assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL + assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL + + +def test_move_avoids_loop(acfactory): + """Test that the message is only moved once. + + This is to avoid busy loop if moved message reappears in the Inbox + or some scanned folder later. + For example, this happens on servers that alias `INBOX.DeltaChat` to `DeltaChat` folder, + so the message moved to `DeltaChat` appears as a new message in the `INBOX.DeltaChat` folder. + We do not want to move this message from `INBOX.DeltaChat` to `DeltaChat` again. + """ + ac1 = acfactory.new_online_configuring_account() + ac2 = acfactory.new_online_configuring_account(mvbox_move=True) + acfactory.bring_accounts_online() + ac1_chat = acfactory.get_accepted_chat(ac1, ac2) + ac1_chat.send_text("Message 1") + + # Message is moved to the DeltaChat folder and downloaded. + ac2_msg1 = ac2._evtracker.wait_next_incoming_message() + assert ac2_msg1.text == "Message 1" + + # Move the message to the INBOX again. + ac2.direct_imap.select_folder("DeltaChat") + ac2.direct_imap.conn.move(["*"], "INBOX") + + ac1_chat.send_text("Message 2") + ac2_msg2 = ac2._evtracker.wait_next_incoming_message() + assert ac2_msg2.text == "Message 2" + + # Check that Message 1 is still in the INBOX folder + # and Message 2 is in the DeltaChat folder. + ac2.direct_imap.select_folder("INBOX") + assert len(ac2.direct_imap.get_all_messages()) == 1 + ac2.direct_imap.select_folder("DeltaChat") + assert len(ac2.direct_imap.get_all_messages()) == 1 def test_move_works_on_self_sent(acfactory): @@ -396,9 +489,12 @@ def test_forward_messages(acfactory, lp): lp.sec("ac2: check new chat has a forwarded message") assert chat3.is_promoted() messages = chat3.get_messages() + assert len(messages) == 1 msg = messages[-1] assert msg.is_forwarded() ac2.delete_messages(messages) + ev = ac2._evtracker.get_matching("DC_EVENT_MSG_DELETED") + assert ev.data2 == messages[0].id assert not chat3.get_messages() @@ -531,8 +627,8 @@ def test_send_and_receive_message_markseen(acfactory, lp): lp.step("1") for _i in range(2): ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ") - assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL - assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL + assert ev.data1 > dc.const.DC_CHAT_ID_LAST_SPECIAL + assert ev.data2 > dc.const.DC_MSG_ID_LAST_SPECIAL lp.step("2") # Check that ac1 marks the read receipt as read. @@ -703,7 +799,7 @@ def test_mdn_asymmetric(acfactory, lp): assert len(chat.get_messages()) == 1 # Wait for the message to be marked as seen on IMAP. - ac1._evtracker.get_info_contains("Marked messages 1 in folder DeltaChat as seen.") + ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.") # MDN is received even though MDNs are already disabled assert msg_out.is_out_mdn_received() @@ -1204,7 +1300,7 @@ def test_quote_encrypted(acfactory, lp): assert msg_in.is_encrypted() == quoted_msg.is_encrypted() -def test_quote_attachment(tmpdir, acfactory, lp): +def test_quote_attachment(tmp_path, acfactory, lp): """Test that replies with an attachment and a quote are received correctly.""" ac1, ac2 = acfactory.get_online_accounts(2) @@ -1219,15 +1315,14 @@ def test_quote_attachment(tmpdir, acfactory, lp): assert received_message.text == "hi" basename = "attachment.txt" - p = os.path.join(tmpdir.strpath, basename) - with open(p, "w") as f: - f.write("data to send") + p = tmp_path / basename + p.write_text("data to send") lp.sec("ac2 sends a reply to ac1") chat2 = received_message.create_chat() reply = Message.new_empty(ac2, "file") reply.set_text("message reply") - reply.set_file(p) + reply.set_file(str(p)) reply.quote = received_message chat2.send_msg(reply) @@ -1334,7 +1429,7 @@ def test_send_and_receive_image(acfactory, lp, data): assert m == msg_in -def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir): +def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path): """See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded messages are received out of order". @@ -1369,23 +1464,27 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir): lp.sec("sending small+large messages from ac1 to ac2") msgs = [] msgs.append(chat.send_text("hi")) - path = tmpdir.join("large") - with open(path, "wb") as fout: - fout.write(os.urandom(download_limit + 1)) - msgs.append(chat.send_file(path.strpath)) - - lp.sec("sending a reaction to the large message from ac1 to ac2") - react_str = "\N{THUMBS UP SIGN}" - msgs.append(msgs[-1].send_reaction(react_str)) - + path = tmp_path / "large" + path.write_bytes(os.urandom(download_limit + 1)) + msgs.append(chat.send_file(str(path))) for m in msgs: ac1._evtracker.wait_msg_delivered(m) + + lp.sec("sending a reaction to the large message from ac1 to ac2") + # TODO: Find the reason of an occasional message reordering on the server (so that the reaction + # has a lower UID than the previous message). W/a is to sleep for some time to let the reaction + # have a later INTERNALDATE. + time.sleep(1.1) + react_str = "\N{THUMBS UP SIGN}" + msgs.append(msgs[-1].send_reaction(react_str)) + ac1._evtracker.wait_msg_delivered(msgs[-1]) + ac2.start_io() lp.sec("wait for ac2 to receive a reaction") msg2 = ac2._evtracker.wait_next_reactions_changed() assert msg2.get_sender_contact().addr == ac1_addr - assert msg2.download_state == const.DC_DOWNLOAD_AVAILABLE + assert msg2.download_state == dc.const.DC_DOWNLOAD_AVAILABLE assert reactions_queue.get() == msg2 reactions = msg2.get_reactions() contacts = reactions.get_contacts() @@ -1411,7 +1510,7 @@ def test_reactions_for_a_reordering_move(acfactory, lp): ac1._evtracker.wait_msg_delivered(msg1) # It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct # order by DC, and most (if not all) mail servers provide only seconds precision. - time.sleep(2) + time.sleep(1.1) react_str = "\N{THUMBS UP SIGN}" ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str)) @@ -1431,7 +1530,7 @@ def test_reactions_for_a_reordering_move(acfactory, lp): assert reactions.get_by_contact(contacts[0]) == react_str -def test_import_export_online_all(acfactory, tmpdir, data, lp): +def test_import_export_online_all(acfactory, tmp_path, data, lp): (ac1,) = acfactory.get_online_accounts(1) lp.sec("create some chat content") @@ -1443,10 +1542,10 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp): chat1.send_image(original_image_path) # Add another 100KB file that ensures that the progress is smooth enough - path = tmpdir.join("attachment.txt") - with open(path, "w") as file: + path = tmp_path / "attachment.txt" + with path.open("w") as file: file.truncate(100000) - chat1.send_file(path.strpath) + chat1.send_file(str(path)) def assert_account_is_proper(ac): contacts = ac.get_contacts(query="some1") @@ -1464,12 +1563,13 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp): assert_account_is_proper(ac1) - backupdir = tmpdir.mkdir("backup") + backupdir = tmp_path / "backup" + backupdir.mkdir() lp.sec(f"export all to {backupdir}") with ac1.temp_plugin(ImexTracker()) as imex_tracker: ac1.stop_io() - ac1.imex(backupdir.strpath, const.DC_IMEX_EXPORT_BACKUP) + ac1.imex(str(backupdir), dc.const.DC_IMEX_EXPORT_BACKUP) # check progress events for export assert imex_tracker.wait_progress(1, progress_upper_limit=249) @@ -1487,7 +1587,7 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp): ac2 = acfactory.get_unconfigured_account() lp.sec("get latest backup file") - path2 = ac2.get_latest_backupfile(backupdir.strpath) + path2 = ac2.get_latest_backupfile(str(backupdir)) assert path2 == path lp.sec("import backup and check it's proper") @@ -1505,10 +1605,10 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp): lp.sec(f"Second-time export all to {backupdir}") ac1.stop_io() - path2 = ac1.export_all(backupdir.strpath) + path2 = ac1.export_all(str(backupdir)) assert os.path.exists(path2) assert path2 != path - assert ac2.get_latest_backupfile(backupdir.strpath) == path2 + assert ac2.get_latest_backupfile(str(backupdir)) == path2 def test_ac_setup_message(acfactory, lp): @@ -1569,8 +1669,12 @@ def test_qr_setup_contact(acfactory, lp): ac1._evtracker.wait_securejoin_inviter_progress(1000) -def test_qr_join_chat(acfactory, lp): +@pytest.mark.parametrize("verified_one_on_one_chats", [0, 1]) +def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats): ac1, ac2 = acfactory.get_online_accounts(2) + ac1.set_config("verified_one_on_one_chats", verified_one_on_one_chats) + ac2.set_config("verified_one_on_one_chats", verified_one_on_one_chats) + lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin") chat = ac1.create_group_chat("hello") qr = chat.get_join_qr() @@ -1583,6 +1687,78 @@ def test_qr_join_chat(acfactory, lp): ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED") ac1._evtracker.wait_securejoin_inviter_progress(1000) + msg = ac2._evtracker.wait_next_incoming_message() + assert msg.text == "Member Me ({}) added by {}.".format(ac2.get_config("addr"), ac1.get_config("addr")) + + # ac1 reloads the chat. + chat = Chat(chat.account, chat.id) + assert not chat.is_protected() + + # ac2 reloads the chat. + ch = Chat(ch.account, ch.id) + assert not ch.is_protected() + + +def test_qr_new_group_unblocked(acfactory, lp): + """Regression test for a bug intoduced in core v1.113.0. + ac2 scans a verified group QR code created by ac1. + This results in creation of a blocked 1:1 chat with ac1 on ac2, + but ac1 contact is not blocked on ac2. + Then ac1 creates a group, adds ac2 there and promotes it by sending a message. + ac2 should receive a message and create a contact request for the group. + Due to a bug previously ac2 created a blocked group. + """ + + ac1, ac2 = acfactory.get_online_accounts(2) + ac1_chat = ac1.create_group_chat("Group for joining", verified=True) + qr = ac1_chat.get_join_qr() + ac2.qr_join_chat(qr) + + ac1._evtracker.wait_securejoin_inviter_progress(1000) + + ac1_new_chat = ac1.create_group_chat("Another group") + ac1_new_chat.add_contact(ac2) + # Receive "Member added" message. + ac2._evtracker.wait_next_incoming_message() + + ac1_new_chat.send_text("Hello!") + ac2_msg = ac2._evtracker.wait_next_incoming_message() + assert ac2_msg.text == "Hello!" + assert ac2_msg.chat.is_contact_request() + + +def test_qr_email_capitalization(acfactory, lp): + """Regression test for a bug + that resulted in failure to propagate verification via gossip in a verified group + when the database already contained the contact with a different email address capitalization. + """ + + ac1, ac2, ac3 = acfactory.get_online_accounts(3) + + # ac1 adds ac2 as a contact with an email address in uppercase. + ac2_addr_uppercase = ac2.get_config("addr").upper() + lp.sec(f"ac1 creates a contact for ac2 ({ac2_addr_uppercase})") + ac1.create_contact(ac2_addr_uppercase) + + lp.sec("ac3 creates a verified group with a QR code") + chat = ac3.create_group_chat("hello", verified=True) + qr = chat.get_join_qr() + + lp.sec("ac1 joins a verified group via a QR code") + ac1_chat = ac1.qr_join_chat(qr) + msg = ac1._evtracker.wait_next_incoming_message() + assert msg.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr")) + assert len(ac1_chat.get_contacts()) == 2 + + lp.sec("ac2 joins a verified group via a QR code") + ac2.qr_join_chat(qr) + ac1._evtracker.wait_next_incoming_message() + + # ac1 should see both ac3 and ac2 as verified. + assert len(ac1_chat.get_contacts()) == 3 + for contact in ac1_chat.get_contacts(): + assert contact.is_verified() + def test_set_get_contact_avatar(acfactory, data, lp): lp.sec("configuring ac1 and ac2") @@ -1768,13 +1944,15 @@ def test_set_get_group_image(acfactory, data, lp): lp.sec("ac1: add ac2 to promoted group chat") chat.add_contact(ac2) # sends one message + lp.sec("ac2: wait for receiving member added message from ac1") + msg1 = ac2._evtracker.wait_next_incoming_message() + assert msg1.is_system_message() # Member added + lp.sec("ac1: send a first message to ac2") chat.send_text("hi") # sends another message assert chat.is_promoted() lp.sec("ac2: wait for receiving message from ac1") - msg1 = ac2._evtracker.wait_next_incoming_message() - assert msg1.is_system_message() # Member added msg2 = ac2._evtracker.wait_next_incoming_message() assert msg2.text == "hi" assert msg1.chat.id == msg2.chat.id @@ -1800,15 +1978,15 @@ def test_connectivity(acfactory, lp): ac1, ac2 = acfactory.get_online_accounts(2) ac1.set_config("scan_all_folders_debounce_secs", "0") - ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTED) + ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED) lp.sec("Test stop_io() and start_io()") ac1.stop_io() - ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED) + ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED) ac1.start_io() - ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING) - ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED) + ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING) + ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTING, dc.const.DC_CONNECTIVITY_CONNECTED) lp.sec( "Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, " @@ -1829,8 +2007,8 @@ def test_connectivity(acfactory, lp): ac2.create_chat(ac1).send_text("Hi 2") - ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING) - ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED) + ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTED, dc.const.DC_CONNECTIVITY_WORKING) + ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED) msgs = ac1.create_chat(ac2).get_messages() assert len(msgs) == 2 @@ -1840,7 +2018,7 @@ def test_connectivity(acfactory, lp): ac1.maybe_network() while 1: - assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED + assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED if ac1.all_work_done(): break ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED") @@ -1855,7 +2033,7 @@ def test_connectivity(acfactory, lp): ac1.maybe_network() while 1: - assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED + assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED if ac1.all_work_done(): break ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED") @@ -1864,10 +2042,10 @@ def test_connectivity(acfactory, lp): ac1.set_config("configured_mail_pw", "abc") ac1.stop_io() - ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED) + ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED) ac1.start_io() - ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING) - ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED) + ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING) + ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED) def test_fetch_deleted_msg(acfactory, lp): @@ -2350,9 +2528,9 @@ def test_archived_muted_chat(acfactory, lp): lp.sec("wait for ac2 to receive DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK") while 1: ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED") - if ev.data1 == const.DC_CHAT_ID_ARCHIVED_LINK: + if ev.data1 == dc.const.DC_CHAT_ID_ARCHIVED_LINK: assert ev.data2 == 0 - archive = ac2.get_chat_by_id(const.DC_CHAT_ID_ARCHIVED_LINK) + archive = ac2.get_chat_by_id(dc.const.DC_CHAT_ID_ARCHIVED_LINK) assert archive.count_fresh_messages() == 1 assert chat2.count_fresh_messages() == 1 break diff --git a/python/tests/test_2_increation.py b/python/tests/test_2_increation.py index c7e35119a..2f053e002 100644 --- a/python/tests/test_2_increation.py +++ b/python/tests/test_2_increation.py @@ -30,28 +30,28 @@ def wait_msgs_changed(account, msgs_list): class TestOnlineInCreation: - def test_increation_not_blobdir(self, tmpdir, acfactory, lp): + def test_increation_not_blobdir(self, tmp_path, acfactory, lp): ac1, ac2 = acfactory.get_online_accounts(2) chat = ac1.create_chat(ac2) lp.sec("Creating in-creation file outside of blobdir") - assert tmpdir.strpath != ac1.get_blobdir() - src = tmpdir.join("file.txt").ensure(file=1) + assert str(tmp_path) != ac1.get_blobdir() + src = tmp_path / "file.txt" + src.touch() with pytest.raises(Exception): - chat.prepare_message_file(src.strpath) + chat.prepare_message_file(str(src)) - def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp): + def test_no_increation_copies_to_blobdir(self, tmp_path, acfactory, lp): ac1, ac2 = acfactory.get_online_accounts(2) chat = ac1.create_chat(ac2) lp.sec("Creating file outside of blobdir") - assert tmpdir.strpath != ac1.get_blobdir() - src = tmpdir.join("file.txt") - src.write("hello there\n") - chat.send_file(src.strpath) - - blob_src = os.path.join(ac1.get_blobdir(), "file.txt") - assert os.path.exists(blob_src), "file.txt not copied to blobdir" + assert str(tmp_path) != ac1.get_blobdir() + src = tmp_path / "file.txt" + src.write_text("hello there\n") + msg = chat.send_file(str(src)) + assert msg.filename.startswith(os.path.join(ac1.get_blobdir(), "file")) + assert msg.filename.endswith(".txt") def test_forward_increation(self, acfactory, data, lp): ac1, ac2 = acfactory.get_online_accounts(2) diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index e9a297681..4b040c65a 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -4,12 +4,11 @@ from datetime import datetime, timedelta, timezone import pytest -from deltachat import Account, const +import deltachat as dc from deltachat.capi import ffi, lib from deltachat.cutil import iter_array -from deltachat.hookspec import account_hookimpl -from deltachat.message import Message from deltachat.tracker import ImexFailed +from deltachat import Account, account_hookimpl, Message @pytest.mark.parametrize( @@ -52,26 +51,25 @@ def test_parse_system_add_remove(msgtext, res): class TestOfflineAccountBasic: - def test_wrong_db(self, tmpdir): - p = tmpdir.join("hello.db") - p.write("123") - account = Account(p.strpath) + def test_wrong_db(self, tmp_path): + p = tmp_path / "hello.db" + p.write_text("123") + account = Account(str(p)) assert not account.is_open() - def test_os_name(self, tmpdir): - p = tmpdir.join("hello.db") + def test_os_name(self, tmp_path): + p = tmp_path / "hello.db" # we can't easily test if os_name is used in X-Mailer # outgoing messages without a full Online test # but we at least check Account accepts the arg - ac1 = Account(p.strpath, os_name="solarpunk") + ac1 = Account(str(p), os_name="solarpunk") ac1.get_info() def test_preconfigure_keypair(self, acfactory, data): ac = acfactory.get_unconfigured_account() - alice_public = data.read_path("key/alice-public.asc") alice_secret = data.read_path("key/alice-secret.asc") - assert alice_public and alice_secret - ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret) + assert alice_secret + ac._preconfigure_keypair("alice@example.org", alice_secret) def test_getinfo(self, acfactory): ac1 = acfactory.get_unconfigured_account() @@ -299,13 +297,13 @@ class TestOfflineChat: assert not d["draft"] if chat.get_draft() is None else chat.get_draft() def test_group_chat_creation_with_translation(self, ac1): - ac1.set_stock_translation(const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s") + ac1.set_stock_translation(dc.const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s") ac1._evtracker.consume_events() with pytest.raises(ValueError): - ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s") + ac1.set_stock_translation(dc.const.DC_STR_FILE, "xyz %1$s") ac1._evtracker.get_matching("DC_EVENT_WARNING") with pytest.raises(ValueError): - ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s") + ac1.set_stock_translation(dc.const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s") ac1._evtracker.get_matching("DC_EVENT_WARNING") with pytest.raises(ValueError): ac1.set_stock_translation(500, "xyz %1$s") @@ -481,6 +479,19 @@ class TestOfflineChat: contact2 = ac1.create_contact("display1 ", "real") assert contact2.name == "real" + def test_send_lots_of_offline_msgs(self, acfactory): + ac1 = acfactory.get_pseudo_configured_account() + ac1.set_config("configured_mail_server", "example.org") + ac1.set_config("configured_mail_user", "example.org") + ac1.set_config("configured_mail_pw", "example.org") + ac1.set_config("configured_send_server", "example.org") + ac1.set_config("configured_send_user", "example.org") + ac1.set_config("configured_send_pw", "example.org") + ac1.start_io() + chat = ac1.create_contact("some1@example.org", name="some1").create_chat() + for i in range(50): + chat.send_text("hello") + def test_create_chat_simple(self, acfactory): ac1 = acfactory.get_pseudo_configured_account() contact1 = ac1.create_contact("some1@example.org", name="some1") @@ -496,22 +507,22 @@ class TestOfflineChat: contact = msg.get_sender_contact() assert contact == ac1.get_self_contact() - def test_import_export_on_unencrypted_acct(self, acfactory, tmpdir): - backupdir = tmpdir.mkdir("backup") + def test_import_export_on_unencrypted_acct(self, acfactory, tmp_path): + backupdir = tmp_path / "backup" + backupdir.mkdir() ac1 = acfactory.get_pseudo_configured_account() chat = ac1.create_contact("some1 ").create_chat() # send a text message msg = chat.send_text("msg1") # send a binary file - bin = tmpdir.join("some.bin") - with bin.open("w") as f: - f.write("\00123" * 10000) - msg = chat.send_file(bin.strpath) + bin = tmp_path / "some.bin" + bin.write_bytes(b"\00123" * 10000) + msg = chat.send_file(str(bin)) contact = msg.get_sender_contact() assert contact == ac1.get_self_contact() - assert not backupdir.listdir() + assert not list(backupdir.iterdir()) ac1.stop_io() - path = ac1.export_all(backupdir.strpath) + path = ac1.export_all(str(backupdir)) assert os.path.exists(path) ac2 = acfactory.get_unconfigured_account() ac2.import_all(path) @@ -525,27 +536,27 @@ class TestOfflineChat: assert messages[0].text == "msg1" assert os.path.exists(messages[1].filename) - def test_import_export_on_encrypted_acct(self, acfactory, tmpdir): + def test_import_export_on_encrypted_acct(self, acfactory, tmp_path): passphrase1 = "passphrase1" passphrase2 = "passphrase2" - backupdir = tmpdir.mkdir("backup") + backupdir = tmp_path / "backup" + backupdir.mkdir() ac1 = acfactory.get_pseudo_configured_account(passphrase=passphrase1) chat = ac1.create_contact("some1 ").create_chat() # send a text message msg = chat.send_text("msg1") # send a binary file - bin = tmpdir.join("some.bin") - with bin.open("w") as f: - f.write("\00123" * 10000) - msg = chat.send_file(bin.strpath) + bin = tmp_path / "some.bin" + bin.write_bytes(b"\00123" * 10000) + msg = chat.send_file(str(bin)) contact = msg.get_sender_contact() assert contact == ac1.get_self_contact() - assert not backupdir.listdir() + assert not list(backupdir.iterdir()) ac1.stop_io() - path = ac1.export_all(backupdir.strpath) + path = ac1.export_all(str(backupdir)) assert os.path.exists(path) ac2 = acfactory.get_unconfigured_account(closed=True) @@ -580,27 +591,27 @@ class TestOfflineChat: assert messages[0].text == "msg1" assert os.path.exists(messages[1].filename) - def test_import_export_with_passphrase(self, acfactory, tmpdir): + def test_import_export_with_passphrase(self, acfactory, tmp_path): passphrase = "test_passphrase" wrong_passphrase = "wrong_passprase" - backupdir = tmpdir.mkdir("backup") + backupdir = tmp_path / "backup" + backupdir.mkdir() ac1 = acfactory.get_pseudo_configured_account() chat = ac1.create_contact("some1 ").create_chat() # send a text message msg = chat.send_text("msg1") # send a binary file - bin = tmpdir.join("some.bin") - with bin.open("w") as f: - f.write("\00123" * 10000) - msg = chat.send_file(bin.strpath) + bin = tmp_path / "some.bin" + bin.write_bytes(b"\00123" * 10000) + msg = chat.send_file(str(bin)) contact = msg.get_sender_contact() assert contact == ac1.get_self_contact() - assert not backupdir.listdir() + assert not list(backupdir.iterdir()) ac1.stop_io() - path = ac1.export_all(backupdir.strpath, passphrase) + path = ac1.export_all(str(backupdir), passphrase) assert os.path.exists(path) ac2 = acfactory.get_unconfigured_account() @@ -619,7 +630,7 @@ class TestOfflineChat: assert messages[0].text == "msg1" assert os.path.exists(messages[1].filename) - def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmpdir): + def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path): """ Test that account passphrase isn't lost if backup failed to be imported. See https://github.com/deltachat/deltachat-core-rust/issues/3379 @@ -627,24 +638,24 @@ class TestOfflineChat: acct_passphrase = "passphrase1" bak_passphrase = "passphrase2" wrong_passphrase = "wrong_passprase" - backupdir = tmpdir.mkdir("backup") + backupdir = tmp_path / "backup" + backupdir.mkdir() ac1 = acfactory.get_pseudo_configured_account() chat = ac1.create_contact("some1 ").create_chat() # send a text message msg = chat.send_text("msg1") # send a binary file - bin = tmpdir.join("some.bin") - with bin.open("w") as f: - f.write("\00123" * 10000) - msg = chat.send_file(bin.strpath) + bin = tmp_path / "some.bin" + bin.write_bytes(b"\00123" * 10000) + msg = chat.send_file(str(bin)) contact = msg.get_sender_contact() assert contact == ac1.get_self_contact() - assert not backupdir.listdir() + assert not list(backupdir.iterdir()) ac1.stop_io() - path = ac1.export_all(backupdir.strpath, bak_passphrase) + path = ac1.export_all(str(backupdir), bak_passphrase) assert os.path.exists(path) ac2 = acfactory.get_unconfigured_account(closed=True) @@ -805,7 +816,7 @@ class TestOfflineChat: lp.sec("check message count of only system messages (without daymarkers)") dc_array = ffi.gc( - lib.dc_get_chat_msgs(ac1._dc_context, chat.id, const.DC_GCM_INFO_ONLY, 0), + lib.dc_get_chat_msgs(ac1._dc_context, chat.id, dc.const.DC_GCM_INFO_ONLY, 0), lib.dc_array_unref, ) assert len(list(iter_array(dc_array, lambda x: x))) == 2 diff --git a/python/tests/test_4_lowlevel.py b/python/tests/test_4_lowlevel.py index 84a751ea4..23ef3dbe3 100644 --- a/python/tests/test_4_lowlevel.py +++ b/python/tests/test_4_lowlevel.py @@ -1,7 +1,8 @@ -import os +import json from queue import Queue -from deltachat import capi, const, cutil, register_global_plugin +import deltachat as dc +from deltachat import capi, cutil, register_global_plugin from deltachat.capi import ffi, lib from deltachat.hookspec import global_hookimpl from deltachat.testplugin import ( @@ -9,6 +10,7 @@ from deltachat.testplugin import ( create_dict_from_files_in_path, write_dict_to_dir, ) +from deltachat.cutil import from_optional_dc_charpointer # from deltachat.account import EventLogger @@ -64,16 +66,17 @@ class TestACSetup: assert pc._account2state[ac1] == pc.IDLEREADY assert pc._account2state[ac2] == pc.IDLEREADY - def test_store_and_retrieve_configured_account_cache(self, acfactory, tmpdir): + def test_store_and_retrieve_configured_account_cache(self, acfactory, tmp_path): ac1 = acfactory.get_pseudo_configured_account() holder = acfactory._acsetup.testprocess assert holder.cache_maybe_store_configured_db_files(ac1) assert not holder.cache_maybe_store_configured_db_files(ac1) - acdir = tmpdir.mkdir("newaccount") + acdir = tmp_path / "newaccount" + acdir.mkdir() addr = ac1.get_config("addr") - target_db_path = acdir.join("db").strpath - assert holder.cache_maybe_retrieve_configured_db_files(addr, target_db_path) - assert len(os.listdir(acdir)) >= 2 + target_db_path = acdir / "db" + assert holder.cache_maybe_retrieve_configured_db_files(addr, str(target_db_path)) + assert sum(1 for _ in acdir.iterdir()) >= 2 def test_liveconfig_caching(acfactory, monkeypatch): @@ -111,40 +114,40 @@ def test_dc_close_events(acfactory): shutdowns.get(timeout=2) -def test_wrong_db(tmpdir): - p = tmpdir.join("hello.db") +def test_wrong_db(tmp_path): + p = tmp_path / "hello.db" # write an invalid database file - p.write("x123" * 10) + p.write_bytes(b"x123" * 10) - context = lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL) + context = lib.dc_context_new(ffi.NULL, str(p).encode("ascii"), ffi.NULL) assert not lib.dc_context_is_open(context) -def test_empty_blobdir(tmpdir): - db_fname = tmpdir.join("hello.db") +def test_empty_blobdir(tmp_path): + db_fname = tmp_path / "hello.db" # Apparently some client code expects this to be the same as passing NULL. ctx = ffi.gc( - lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), b""), + lib.dc_context_new(ffi.NULL, str(db_fname).encode("ascii"), b""), lib.dc_context_unref, ) assert ctx != ffi.NULL def test_event_defines(): - assert const.DC_EVENT_INFO == 100 - assert const.DC_CONTACT_ID_SELF + assert dc.const.DC_EVENT_INFO == 100 + assert dc.const.DC_CONTACT_ID_SELF def test_sig(): sig = capi.lib.dc_event_has_string_data - assert not sig(const.DC_EVENT_MSGS_CHANGED) - assert sig(const.DC_EVENT_INFO) - assert sig(const.DC_EVENT_WARNING) - assert sig(const.DC_EVENT_ERROR) - assert sig(const.DC_EVENT_SMTP_CONNECTED) - assert sig(const.DC_EVENT_IMAP_CONNECTED) - assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT) - assert sig(const.DC_EVENT_IMEX_FILE_WRITTEN) + assert not sig(dc.const.DC_EVENT_MSGS_CHANGED) + assert sig(dc.const.DC_EVENT_INFO) + assert sig(dc.const.DC_EVENT_WARNING) + assert sig(dc.const.DC_EVENT_ERROR) + assert sig(dc.const.DC_EVENT_SMTP_CONNECTED) + assert sig(dc.const.DC_EVENT_IMAP_CONNECTED) + assert sig(dc.const.DC_EVENT_SMTP_MESSAGE_SENT) + assert sig(dc.const.DC_EVENT_IMEX_FILE_WRITTEN) def test_markseen_invalid_message_ids(acfactory): @@ -173,10 +176,10 @@ def test_provider_info_none(): assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL -def test_get_info_open(tmpdir): - db_fname = tmpdir.join("test.db") +def test_get_info_open(tmp_path): + db_fname = tmp_path / "test.db" ctx = ffi.gc( - lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL), + lib.dc_context_new(ffi.NULL, str(db_fname).encode("ascii"), ffi.NULL), lib.dc_context_unref, ) info = cutil.from_dc_charpointer(lib.dc_get_info(ctx)) @@ -215,3 +218,36 @@ def test_logged_ac_process_ffi_failure(acfactory): assert "ac_process_ffi_event" in res assert "ZeroDivisionError" in res assert "Traceback" in res + + +def test_jsonrpc_blocking_call(tmp_path): + accounts_fname = tmp_path / "accounts" + writable = True + accounts = ffi.gc( + lib.dc_accounts_new(str(accounts_fname).encode("ascii"), writable), + lib.dc_accounts_unref, + ) + jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref) + res = json.loads( + from_optional_dc_charpointer( + lib.dc_jsonrpc_blocking_call( + jsonrpc, + json.dumps( + {"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice@example.org"], "id": "123"}, + ).encode("utf-8"), + ), + ), + ) + assert res == {"jsonrpc": "2.0", "id": "123", "result": True} + + res = json.loads( + from_optional_dc_charpointer( + lib.dc_jsonrpc_blocking_call( + jsonrpc, + json.dumps( + {"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice"], "id": "456"}, + ).encode("utf-8"), + ), + ), + ) + assert res == {"jsonrpc": "2.0", "id": "456", "result": False} diff --git a/python/tox.ini b/python/tox.ini index 99fd0c703..ed6069dec 100644 --- a/python/tox.ini +++ b/python/tox.ini @@ -25,6 +25,9 @@ deps = pytest-xdist pdbpp requests +# urllib3 2.0 does not work in manylinux2014 containers. +# https://github.com/deltachat/deltachat-core-rust/issues/4788 + urllib3<2 [testenv:.pkg] passenv = @@ -59,7 +62,8 @@ commands = [testenv:doc] changedir=doc deps = - sphinx +# Pinned due to incompatibility of breathe with sphinx 7.2: + sphinx<=7.1.2 breathe commands = sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html diff --git a/release-date.in b/release-date.in index 5dc1be6ba..666f14451 100644 --- a/release-date.in +++ b/release-date.in @@ -1 +1 @@ -2023-04-04 \ No newline at end of file +2023-10-24 \ No newline at end of file diff --git a/release.toml b/release.toml deleted file mode 100644 index 21181de6c..000000000 --- a/release.toml +++ /dev/null @@ -1,3 +0,0 @@ -pre-release-commit-message = "chore({{crate_name}}): release {{version}}" -pro-release-commit-message = "chore({{crate_name}}): starting development cycle for {{next_version}}" -no-dev-version = true \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md index 6b2baac37..551d7e77a 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -10,6 +10,8 @@ and an own build machine. - `deny.sh` runs `cargo deny` for all Rust code in the project. +- `codespell.sh` spellchecks the source code using `codespell` tool. + - `../.github/workflows` contains jobs run by GitHub Actions. - `remote_tests_python.sh` rsyncs to a build machine and runs @@ -18,6 +20,10 @@ and an own build machine. - `remote_tests_rust.sh` rsyncs to the build machine and runs `run-rust-test.sh` remotely on the build machine. +- `make-python-testenv.sh` creates local python test development environment. + Reusing the same environment is faster than running `run-python-test.sh` which always + recreates environment from scratch and runs additional lints. + - `run-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/ - `run_all.sh` builds Python wheels diff --git a/scripts/concourse/README.md b/scripts/concourse/README.md index b022d9bc4..ad1882f41 100644 --- a/scripts/concourse/README.md +++ b/scripts/concourse/README.md @@ -12,10 +12,15 @@ where `secret.yml` contains the following secrets: ``` c.delta.chat: private_key: | - -----BEGIN RSA PRIVATE KEY----- + -----BEGIN OPENSSH PRIVATE KEY----- ... - -----END RSA PRIVATE KEY----- + -----END OPENSSH PRIVATE KEY----- devpi: login: dc password: ... ``` + +Secrets can be read from the password manager: +``` +fly -t b1 set-pipeline -c docs_wheels.yml -p docs_wheels -l <(pass show delta/b1.delta.chat/secret.yml) +``` diff --git a/scripts/concourse/docs_wheels.yml b/scripts/concourse/docs_wheels.yml index 3f461ad20..cc65d00fd 100644 --- a/scripts/concourse/docs_wheels.yml +++ b/scripts/concourse/docs_wheels.yml @@ -153,11 +153,13 @@ jobs: - -ec - | apt-get update -y - apt-get install -y --no-install-recommends python3-pip python3-setuptools - pip3 install devpi - devpi use https://m.devpi.net/dc/master - devpi login ((devpi.login)) --password ((devpi.password)) - devpi upload py-wheels/*manylinux201* + apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv + python3 -m venv env + env/bin/pip install --upgrade pip + env/bin/pip install devpi + env/bin/devpi use https://m.devpi.net/dc/master + env/bin/devpi login ((devpi.login)) --password ((devpi.password)) + env/bin/devpi upload py-wheels/*manylinux201* - name: python-aarch64 plan: @@ -223,11 +225,13 @@ jobs: - -ec - | apt-get update -y - apt-get install -y --no-install-recommends python3-pip python3-setuptools - pip3 install devpi - devpi use https://m.devpi.net/dc/master - devpi login ((devpi.login)) --password ((devpi.password)) - devpi upload py-wheels/*manylinux201* + apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv + python3 -m venv env + env/bin/pip install --upgrade pip + env/bin/pip install devpi + env/bin/devpi use https://m.devpi.net/dc/master + env/bin/devpi login ((devpi.login)) --password ((devpi.password)) + env/bin/devpi upload py-wheels/*manylinux201* - name: python-musl-x86_64 plan: @@ -293,11 +297,13 @@ jobs: - -ec - | apt-get update -y - apt-get install -y --no-install-recommends python3-pip python3-setuptools - pip3 install devpi - devpi use https://m.devpi.net/dc/master - devpi login ((devpi.login)) --password ((devpi.password)) - devpi upload py-wheels/*musllinux_1_1_x86_64* + apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv + python3 -m venv env + env/bin/pip install --upgrade pip + env/bin/pip install devpi + env/bin/devpi use https://m.devpi.net/dc/master + env/bin/devpi login ((devpi.login)) --password ((devpi.password)) + env/bin/devpi upload py-wheels/*musllinux_1_1_x86_64* - name: python-musl-aarch64 plan: @@ -363,8 +369,10 @@ jobs: - -ec - | apt-get update -y - apt-get install -y --no-install-recommends python3-pip python3-setuptools - pip3 install devpi - devpi use https://m.devpi.net/dc/master - devpi login ((devpi.login)) --password ((devpi.password)) - devpi upload py-wheels/*musllinux_1_1_aarch64* + apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv + python3 -m venv env + env/bin/pip install --upgrade pip + env/bin/pip install devpi + env/bin/devpi use https://m.devpi.net/dc/master + env/bin/devpi login ((devpi.login)) --password ((devpi.password)) + env/bin/devpi upload py-wheels/*musllinux_1_1_aarch64* diff --git a/scripts/coredeps/Dockerfile b/scripts/coredeps/Dockerfile index a6ceed532..55b90d9f6 100644 --- a/scripts/coredeps/Dockerfile +++ b/scripts/coredeps/Dockerfile @@ -6,3 +6,4 @@ FROM $BASEIMAGE RUN pipx install tox COPY install-rust.sh /scripts/ RUN /scripts/install-rust.sh +RUN if command -v yum; then yum install -y perl-IPC-Cmd; fi diff --git a/scripts/coredeps/install-rust.sh b/scripts/coredeps/install-rust.sh index 8181cc991..20e72611b 100755 --- a/scripts/coredeps/install-rust.sh +++ b/scripts/coredeps/install-rust.sh @@ -7,7 +7,7 @@ set -euo pipefail # # Avoid using rustup here as it depends on reading /proc/self/exe and # has problems running under QEMU. -RUST_VERSION=1.68.0 +RUST_VERSION=1.72.0 ARCH="$(uname -m)" test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu diff --git a/scripts/create-provider-data-rs.py b/scripts/create-provider-data-rs.py index 824e3354d..589cd0b90 100755 --- a/scripts/create-provider-data-rs.py +++ b/scripts/create-provider-data-rs.py @@ -44,7 +44,7 @@ def file2url(f): def process_opt(data): if not "opt" in data: - return "Default::default()" + return "ProviderOptions::new()" opt = "ProviderOptions {\n" opt_data = data.get("opt", "") for key in opt_data: @@ -54,7 +54,7 @@ def process_opt(data): if value in {"True", "False"}: value = value.lower() opt += " " + key + ": " + value + ",\n" - opt += " ..Default::default()\n" + opt += " ..ProviderOptions::new()\n" opt += " }" return opt @@ -62,7 +62,7 @@ def process_opt(data): def process_config_defaults(data): if not "config_defaults" in data: return "None" - defaults = "Some(vec![\n" + defaults = "Some(&[\n" config_defaults = data.get("config_defaults", "") for key in config_defaults: value = str(config_defaults[key]) @@ -96,11 +96,11 @@ def process_data(data, file): raise TypeError("domain used twice: " + domain) domains_set.add(domain) - domains += ' ("' + domain + '", &*' + file2varname(file) + "),\n" + domains += ' ("' + domain + '", &' + file2varname(file) + "),\n" comment += domain + ", " ids = "" - ids += ' ("' + file2id(file) + '", &*' + file2varname(file) + "),\n" + ids += ' ("' + file2id(file) + '", &' + file2varname(file) + "),\n" server = "" has_imap = False @@ -155,18 +155,18 @@ def process_data(data, file): provider += ( "static " + file2varname(file) - + ": Lazy = Lazy::new(|| Provider {\n" + + ": Provider = Provider {\n" ) provider += ' id: "' + file2id(file) + '",\n' provider += " status: Status::" + status.capitalize() + ",\n" provider += ' before_login_hint: "' + before_login_hint + '",\n' provider += ' after_login_hint: "' + after_login_hint + '",\n' provider += ' overview_page: "' + file2url(file) + '",\n' - provider += " server: vec![\n" + server + " ],\n" + provider += " server: &[\n" + server + " ],\n" provider += " opt: " + opt + ",\n" provider += " config_defaults: " + config_defaults + ",\n" provider += " oauth2_authorizer: " + oauth2 + ",\n" - provider += "});\n\n" + provider += "};\n\n" else: raise TypeError("SMTP and IMAP must be specified together or left out both") diff --git a/scripts/deny.sh b/scripts/deny.sh index 1b6c9ac0b..05c508b23 100755 --- a/scripts/deny.sh +++ b/scripts/deny.sh @@ -1,2 +1,6 @@ #!/bin/sh + +# Update package cache without changing the lockfile. +cargo update --dry-run + cargo deny --workspace --all-features check -D warnings diff --git a/scripts/make-python-testenv.sh b/scripts/make-python-testenv.sh new file mode 100755 index 000000000..ce3d0b71e --- /dev/null +++ b/scripts/make-python-testenv.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# +# Script to create or update a python development environment. +# It rebuilds the core and bindings as needed. +# +# After running the script, you can either +# run `pytest` directly with `venv/bin/pytest python/` +# or activate the environment with `. venv/bin/activate` +# and run `pytest` from there. +set -euo pipefail + +export DCC_RS_TARGET=debug +export DCC_RS_DEV="$PWD" +cargo build -p deltachat_ffi --features jsonrpc + +tox -c python -e py --devenv venv +venv/bin/pip install --upgrade pip diff --git a/scripts/run_all.sh b/scripts/run_all.sh index 87ce41ea3..42edb094f 100755 --- a/scripts/run_all.sh +++ b/scripts/run_all.sh @@ -31,7 +31,7 @@ unset DCC_NEW_TMP_EMAIL # Try to build wheels for a range of interpreters, but don't fail if they are not available. # E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10 -tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39 --skip-missing-interpreters true +tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse" diff --git a/scripts/set_core_version.py b/scripts/set_core_version.py index cd6303cc9..0958c1669 100755 --- a/scripts/set_core_version.py +++ b/scripts/set_core_version.py @@ -7,6 +7,7 @@ import pathlib import re import subprocess from argparse import ArgumentParser +from pathlib import Path rex = re.compile(r'version = "(\S+)"') @@ -95,22 +96,14 @@ def main(): today = datetime.date.today().isoformat() if "alpha" not in newversion: - changelog_name = "CHANGELOG.md" - changelog_tmpname = changelog_name + ".tmp" - changelog_tmp = open(changelog_tmpname, "w") found = False - for line in open(changelog_name): - ## 1.25.0 - if line == f"## [{newversion}]\n": - line = f"## [{newversion}] - {today}\n" + for line in Path("CHANGELOG.md").open(): + if line == f"## [{newversion}] - {today}\n": found = True - changelog_tmp.write(line) if not found: raise SystemExit( f"{changelog_name} contains no entry for version: {newversion}" ) - changelog_tmp.close() - os.rename(changelog_tmpname, changelog_name) for toml_filename in toml_list: replace_toml_version(toml_filename, newversion) @@ -128,11 +121,15 @@ def main(): subprocess.call(["git", "add", "-u"]) # subprocess.call(["cargo", "update", "-p", "deltachat"]) - print("after commit, on master make sure to: ") - print("") + print("After commit, make sure to:") + print() print(f" git tag -a v{newversion}") print(f" git push origin v{newversion}") - print("") + print(f" gh release create v{newversion} -n ''") + print() + print("Merge release branch into `master` if the release") + print("is made on a stable branch.") + print() if __name__ == "__main__": diff --git a/scripts/wheel-rpc-server.py b/scripts/wheel-rpc-server.py new file mode 100755 index 000000000..41f97af95 --- /dev/null +++ b/scripts/wheel-rpc-server.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Build Python wheels for deltachat-rpc-server. +Run scripts/zig-rpc-server.sh first.""" +from pathlib import Path +from wheel.wheelfile import WheelFile +import tomllib +import tarfile +from io import BytesIO + + +def metadata_contents(version): + return f"""Metadata-Version: 2.1 +Name: deltachat-rpc-server +Version: {version} +Summary: Delta Chat JSON-RPC server +""" + + +def build_source_package(version): + filename = f"dist/deltachat-rpc-server-{version}.tar.gz" + + with tarfile.open(filename, "w:gz") as pkg: + + def pack(name, contents): + contents = contents.encode() + tar_info = tarfile.TarInfo(f"deltachat-rpc-server-{version}/{name}") + tar_info.mode = 0o644 + tar_info.size = len(contents) + pkg.addfile(tar_info, BytesIO(contents)) + + pack("PKG-INFO", metadata_contents(version)) + pack( + "pyproject.toml", + f"""[build-system] +requires = ["setuptools==68.2.2", "pip"] +build-backend = "setuptools.build_meta" + +[project] +name = "deltachat-rpc-server" +version = "{version}" + +[project.scripts] +deltachat-rpc-server = "deltachat_rpc_server:main" +""", + ) + pack( + "setup.py", + f""" +import sys +from setuptools import setup, find_packages +from distutils.cmd import Command +from setuptools.command.install import install +from setuptools.command.build import build +import subprocess +import platform +import tempfile +from zipfile import ZipFile +from pathlib import Path +import shutil + + +class BuildCommand(build): + def run(self): + tmpdir = tempfile.mkdtemp() + subprocess.run( + [ + sys.executable, + "-m", + "pip", + "download", + "--no-input", + "--timeout", + "1000", + "--platform", + "musllinux_1_1_" + platform.machine(), + "--only-binary=:all:", + "deltachat-rpc-server=={version}", + ], + cwd=tmpdir, + ) + + wheel_path = next(Path(tmpdir).glob("*.whl")) + with ZipFile(wheel_path, "r") as wheel: + exe_path = wheel.extract("deltachat_rpc_server/deltachat-rpc-server", "src") + Path(exe_path).chmod(0o700) + wheel.extract("deltachat_rpc_server/__init__.py", "src") + + shutil.rmtree(tmpdir) + return super().run() + + +setup( + cmdclass={{"build": BuildCommand}}, + package_data={{"deltachat_rpc_server": ["deltachat-rpc-server"]}}, +) +""", + ) + pack("src/deltachat_rpc_server/__init__.py", "") + + +def build_wheel(version, binary, tag, windows=False): + filename = f"dist/deltachat_rpc_server-{version}-{tag}.whl" + + with WheelFile(filename, "w") as wheel: + wheel.write("LICENSE", "deltachat_rpc_server/LICENSE") + wheel.write("deltachat-rpc-server/README.md", "deltachat_rpc_server/README.md") + if windows: + wheel.writestr( + "deltachat_rpc_server/__init__.py", + """import os, sys, subprocess +def main(): + argv = [os.path.join(os.path.dirname(__file__), "deltachat-rpc-server.exe"), *sys.argv[1:]] + sys.exit(subprocess.call(argv)) +""", + ) + else: + wheel.writestr( + "deltachat_rpc_server/__init__.py", + """import os, sys +def main(): + argv = [os.path.join(os.path.dirname(__file__), "deltachat-rpc-server"), *sys.argv[1:]] + os.execv(argv[0], argv) +""", + ) + + Path(binary).chmod(0o755) + wheel.write( + binary, + "deltachat_rpc_server/deltachat-rpc-server.exe" + if windows + else "deltachat_rpc_server/deltachat-rpc-server", + ) + wheel.writestr( + f"deltachat_rpc_server-{version}.dist-info/METADATA", + metadata_contents(version), + ) + wheel.writestr( + f"deltachat_rpc_server-{version}.dist-info/WHEEL", + "Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: {tag}", + ) + wheel.writestr( + f"deltachat_rpc_server-{version}.dist-info/entry_points.txt", + "[console_scripts]\ndeltachat-rpc-server = deltachat_rpc_server:main", + ) + + +def main(): + with open("deltachat-rpc-server/Cargo.toml", "rb") as f: + cargo_toml = tomllib.load(f) + version = cargo_toml["package"]["version"] + Path("dist").mkdir(exist_ok=True) + build_source_package(version) + build_wheel( + version, + "dist/deltachat-rpc-server-x86_64-linux", + "py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64", + ) + build_wheel( + version, + "dist/deltachat-rpc-server-armv7-linux", + "py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l", + ) + build_wheel( + version, + "dist/deltachat-rpc-server-aarch64-linux", + "py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64", + ) + build_wheel( + version, + "dist/deltachat-rpc-server-i686-linux", + "py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686", + ) + + # macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html + build_wheel( + version, + "dist/deltachat-rpc-server-x86_64-macos", + "py3-none-macosx_10_7_x86_64", + ) + build_wheel( + version, + "dist/deltachat-rpc-server-aarch64-macos", + "py3-none-macosx_11_0_arm64", + ) + + build_wheel( + version, "dist/deltachat-rpc-server-win32.exe", "py3-none-win32", windows=True + ) + build_wheel( + version, + "dist/deltachat-rpc-server-win64.exe", + "py3-none-win_amd64", + windows=True, + ) + + +main() diff --git a/scripts/zig-cc b/scripts/zig-cc index 4b1293dde..ffa3278b4 100755 --- a/scripts/zig-cc +++ b/scripts/zig-cc @@ -1,10 +1,20 @@ #!/usr/bin/env python +# /// pyproject +# [run] +# dependencies = [ +# "ziglang==0.11.0" +# ] +# /// +import os import subprocess import sys -import os def flag_filter(flag: str) -> bool: + # Workaround for . + if flag == "-latomic": + return False + if flag == "-lc": return False if flag == "-Wl,-melf_i386": @@ -24,8 +34,23 @@ def main(): else: zig_cpu_args = [] + # Disable atomics and use locks instead in OpenSSL. + # Zig toolchains do not provide atomics. + # This is a workaround for + args += ["-DBROKEN_CLANG_ATOMICS"] + subprocess.run( - ["zig", "cc", "-target", zig_target, *zig_cpu_args, *args], check=True + [ + sys.executable, + "-m", + "ziglang", + "cc", + "-target", + zig_target, + *zig_cpu_args, + *args, + ], + check=True, ) diff --git a/scripts/zig-rpc-server.sh b/scripts/zig-rpc-server.sh index e411b20bb..455c0f150 100755 --- a/scripts/zig-rpc-server.sh +++ b/scripts/zig-rpc-server.sh @@ -8,15 +8,7 @@ set -e unset RUSTFLAGS # Pin Rust version to avoid uncontrolled changes in the compiler and linker flags. -export RUSTUP_TOOLCHAIN=1.68.1 - -ZIG_VERSION=0.11.0-dev.2213+515e1c93e - -# Download Zig -rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz" -wget "https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz" -tar xf "zig-linux-x86_64-$ZIG_VERSION.tar.xz" -export PATH="$PWD/zig-linux-x86_64-$ZIG_VERSION:$PATH" +export RUSTUP_TOOLCHAIN=1.72.0 rustup target add i686-unknown-linux-musl CC="$PWD/scripts/zig-cc" \ @@ -50,3 +42,9 @@ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \ LD="$PWD/scripts/zig-cc" \ ZIG_TARGET="aarch64-linux-musl" \ cargo build --release --target aarch64-unknown-linux-musl -p deltachat-rpc-server --features vendored + +mkdir -p dist +cp target/x86_64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-linux +cp target/i686-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-i686-linux +cp target/aarch64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-linux +cp target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server dist/deltachat-rpc-server-armv7-linux diff --git a/spec.md b/spec.md index b12a75464..ea34862aa 100644 --- a/spec.md +++ b/spec.md @@ -43,7 +43,7 @@ the `Subject` header SHOULD be `Message from `. Replies to messages MAY follow the typical `Re:`-format. The body MAY contain text which MUST have the content type `text/plain` -or `mulipart/alternative` containing `text/plain`. +or `multipart/alternative` containing `text/plain`. The text MAY be divided into a user-text-part and a footer-part using the line `-- ` (minus, minus, space, lineend). diff --git a/src/accounts.rs b/src/accounts.rs index 082057595..7cbe1cea4 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -7,6 +7,9 @@ use anyhow::{ensure, Context as _, Result}; use serde::{Deserialize, Serialize}; use tokio::fs; use tokio::io::AsyncWriteExt; +use tokio::sync::oneshot; +use tokio::task::JoinHandle; +use tokio::time::{sleep, Duration}; use uuid::Uuid; use crate::context::Context; @@ -18,6 +21,7 @@ use crate::stock_str::StockStrings; pub struct Accounts { dir: PathBuf, config: Config, + /// Map from account ID to the account. accounts: BTreeMap, /// Event channel to emit account manager errors. @@ -32,16 +36,16 @@ pub struct Accounts { impl Accounts { /// Loads or creates an accounts folder at the given `dir`. - pub async fn new(dir: PathBuf) -> Result { - if !dir.exists() { + pub async fn new(dir: PathBuf, writable: bool) -> Result { + if writable && !dir.exists() { Accounts::create(&dir).await?; } - Accounts::open(dir).await + Accounts::open(dir, writable).await } /// Creates a new default structure. - pub async fn create(dir: &Path) -> Result<()> { + async fn create(dir: &Path) -> Result<()> { fs::create_dir_all(dir) .await .context("failed to create folder")?; @@ -53,13 +57,13 @@ impl Accounts { /// Opens an existing accounts structure. Will error if the folder doesn't exist, /// no account exists and no config exists. - pub async fn open(dir: PathBuf) -> Result { + async fn open(dir: PathBuf, writable: bool) -> Result { ensure!(dir.exists(), "directory does not exist"); let config_file = dir.join(CONFIG_NAME); ensure!(config_file.exists(), "{:?} does not exist", config_file); - let config = Config::from_file(config_file) + let config = Config::from_file(config_file, writable) .await .context("failed to load accounts config")?; let events = Events::new(); @@ -78,12 +82,12 @@ impl Accounts { }) } - /// Get an account by its `id`: + /// Returns an account by its `id`: pub fn get_account(&self, id: u32) -> Option { self.accounts.get(&id).cloned() } - /// Get the currently selected account. + /// Returns the currently selected account. pub fn get_selected_account(&self) -> Option { let id = self.config.get_selected_account(); self.accounts.get(&id).cloned() @@ -97,14 +101,14 @@ impl Accounts { } } - /// Select the given account. + /// Selects the given account. pub async fn select_account(&mut self, id: u32) -> Result<()> { self.config.select_account(id).await?; Ok(()) } - /// Add a new account and opens it. + /// Adds a new account and opens it. /// /// Returns account ID. pub async fn add_account(&mut self) -> Result { @@ -139,7 +143,7 @@ impl Accounts { Ok(account_config.id) } - /// Remove an account. + /// Removes an account. pub async fn remove_account(&mut self, id: u32) -> Result<()> { let ctx = self .accounts @@ -160,7 +164,7 @@ impl Accounts { Ok(()) } - /// Migrate an existing account into this structure. + /// Migrates an existing account into this structure. /// /// Returns the ID of new account. pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result { @@ -295,16 +299,22 @@ impl Accounts { } /// Configuration file name. -pub const CONFIG_NAME: &str = "accounts.toml"; +const CONFIG_NAME: &str = "accounts.toml"; + +/// Lockfile name. +const LOCKFILE_NAME: &str = "accounts.lock"; /// Database file name. -pub const DB_NAME: &str = "dc.db"; +const DB_NAME: &str = "dc.db"; /// Account manager configuration file. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug)] struct Config { file: PathBuf, inner: InnerConfig, + // We lock the lockfile in the Config constructors to protect also from having multiple Config + // objects for the same config file. + lock_task: Option>>, } /// Account manager configuration file contents. @@ -318,17 +328,74 @@ struct InnerConfig { pub accounts: Vec, } +impl Drop for Config { + fn drop(&mut self) { + if let Some(lock_task) = self.lock_task.take() { + lock_task.abort(); + } + } +} + impl Config { - /// Creates a new configuration file in the given account manager directory. - pub async fn new(dir: &Path) -> Result { + /// Creates a new Config for `file`, but doesn't open/sync it. + async fn new_nosync(file: PathBuf, lock: bool) -> Result { + let dir = file.parent().context("Cannot get config file directory")?; let inner = InnerConfig { accounts: Vec::new(), selected_account: 0, next_id: 1, }; - let file = dir.join(CONFIG_NAME); - let mut cfg = Self { file, inner }; + if !lock { + let cfg = Self { + file, + inner, + lock_task: None, + }; + return Ok(cfg); + } + let lockfile = dir.join(LOCKFILE_NAME); + let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?); + let (locked_tx, locked_rx) = oneshot::channel(); + let lock_task: JoinHandle> = tokio::spawn(async move { + let mut timeout = Duration::from_millis(100); + let _guard = loop { + match lock.try_write() { + Ok(guard) => break Ok(guard), + Err(err) => { + if timeout.as_millis() > 1600 { + break Err(err); + } + // We need to wait for the previous lock_task to be aborted thus unlocking + // the lockfile. We don't open configs for writing often outside of the + // tests, so this adds delays to the tests, but otherwise ok. + sleep(timeout).await; + if err.kind() == std::io::ErrorKind::WouldBlock { + timeout *= 2; + } + } + } + }?; + locked_tx + .send(()) + .ok() + .context("Cannot notify about lockfile locking")?; + let (_tx, rx) = oneshot::channel(); + rx.await?; + Ok(()) + }); + let cfg = Self { + file, + inner, + lock_task: Some(lock_task), + }; + locked_rx.await?; + Ok(cfg) + } + /// Creates a new configuration file in the given account manager directory. + pub async fn new(dir: &Path) -> Result { + let lock = true; + let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?; cfg.sync().await?; Ok(cfg) @@ -338,6 +405,11 @@ impl Config { /// Takes a mutable reference because the saved file is a part of the `Config` state. This /// protects from parallel calls resulting to a wrong file contents. async fn sync(&mut self) -> Result<()> { + ensure!(!self + .lock_task + .as_ref() + .context("Config is read-only")? + .is_finished()); let tmp_path = self.file.with_extension("toml.tmp"); let mut file = fs::File::create(&tmp_path) .await @@ -356,24 +428,28 @@ impl Config { } /// Read a configuration from the given file into memory. - pub async fn from_file(file: PathBuf) -> Result { - let dir = file.parent().context("can't get config file directory")?; - let bytes = fs::read(&file).await.context("failed to read file")?; + pub async fn from_file(file: PathBuf, writable: bool) -> Result { + let dir = file + .parent() + .context("Cannot get config file directory")? + .to_path_buf(); + let mut config = Self::new_nosync(file, writable).await?; + let bytes = fs::read(&config.file) + .await + .context("Failed to read file")?; let s = std::str::from_utf8(&bytes)?; - let mut inner: InnerConfig = toml::from_str(s).context("failed to parse config")?; + config.inner = toml::from_str(s).context("Failed to parse config")?; // Previous versions of the core stored absolute paths in account config. // Convert them to relative paths. let mut modified = false; - for account in &mut inner.accounts { - if let Ok(new_dir) = account.dir.strip_prefix(dir) { + for account in &mut config.inner.accounts { + if let Ok(new_dir) = account.dir.strip_prefix(&dir) { account.dir = new_dir.to_path_buf(); modified = true; } } - - let mut config = Self { file, inner }; - if modified { + if modified && writable { config.sync().await?; } @@ -517,26 +593,44 @@ mod tests { let p: PathBuf = dir.path().join("accounts1"); { - let mut accounts = Accounts::new(p.clone()).await.unwrap(); + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await.unwrap(); accounts.add_account().await.unwrap(); assert_eq!(accounts.accounts.len(), 1); assert_eq!(accounts.config.get_selected_account(), 1); } - { - let accounts = Accounts::open(p).await.unwrap(); + for writable in [true, false] { + let accounts = Accounts::new(p.clone(), writable).await.unwrap(); assert_eq!(accounts.accounts.len(), 1); assert_eq!(accounts.config.get_selected_account(), 1); } } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_account_new_open_conflict() { + let dir = tempfile::tempdir().unwrap(); + let p: PathBuf = dir.path().join("accounts"); + let writable = true; + let _accounts = Accounts::new(p.clone(), writable).await.unwrap(); + + let writable = true; + assert!(Accounts::new(p.clone(), writable).await.is_err()); + + let writable = false; + let accounts = Accounts::new(p, writable).await.unwrap(); + assert_eq!(accounts.accounts.len(), 0); + assert_eq!(accounts.config.get_selected_account(), 0); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_account_new_add_remove() { let dir = tempfile::tempdir().unwrap(); let p: PathBuf = dir.path().join("accounts"); - let mut accounts = Accounts::new(p.clone()).await.unwrap(); + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await.unwrap(); assert_eq!(accounts.accounts.len(), 0); assert_eq!(accounts.config.get_selected_account(), 0); @@ -563,7 +657,8 @@ mod tests { let dir = tempfile::tempdir()?; let p: PathBuf = dir.path().join("accounts"); - let mut accounts = Accounts::new(p.clone()).await?; + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await?; assert!(accounts.get_selected_account().is_none()); assert_eq!(accounts.config.get_selected_account(), 0); @@ -584,7 +679,8 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let p: PathBuf = dir.path().join("accounts"); - let mut accounts = Accounts::new(p.clone()).await.unwrap(); + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await.unwrap(); assert_eq!(accounts.accounts.len(), 0); assert_eq!(accounts.config.get_selected_account(), 0); @@ -621,7 +717,8 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let p: PathBuf = dir.path().join("accounts"); - let mut accounts = Accounts::new(p.clone()).await.unwrap(); + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await.unwrap(); for expected_id in 1..10 { let id = accounts.add_account().await.unwrap(); @@ -641,7 +738,8 @@ mod tests { let dummy_accounts = 10; let (id0, id1, id2) = { - let mut accounts = Accounts::new(p.clone()).await?; + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await?; accounts.add_account().await?; let ids = accounts.get_all(); assert_eq!(ids.len(), 1); @@ -676,7 +774,8 @@ mod tests { assert!(id2 > id1 + dummy_accounts); let (id0_reopened, id1_reopened, id2_reopened) = { - let accounts = Accounts::new(p.clone()).await?; + let writable = false; + let accounts = Accounts::new(p.clone(), writable).await?; let ctx = accounts.get_selected_account().unwrap(); assert_eq!( ctx.get_config(crate::config::Config::Addr).await?, @@ -721,7 +820,8 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let p: PathBuf = dir.path().join("accounts"); - let accounts = Accounts::new(p.clone()).await?; + let writable = true; + let accounts = Accounts::new(p.clone(), writable).await?; // Make sure there are no accounts. assert_eq!(accounts.accounts.len(), 0); @@ -747,7 +847,8 @@ mod tests { let dir = tempfile::tempdir().context("failed to create tempdir")?; let p: PathBuf = dir.path().join("accounts"); - let mut accounts = Accounts::new(p.clone()) + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable) .await .context("failed to create accounts manager")?; @@ -767,7 +868,8 @@ mod tests { assert!(passphrase_set_success); drop(accounts); - let accounts = Accounts::new(p.clone()) + let writable = false; + let accounts = Accounts::new(p.clone(), writable) .await .context("failed to create second accounts manager")?; let account = accounts @@ -791,7 +893,8 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let p: PathBuf = dir.path().join("accounts"); - let mut accounts = Accounts::new(p.clone()).await?; + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await?; accounts.add_account().await?; accounts.add_account().await?; diff --git a/src/authres.rs b/src/authres.rs index cc684e544..2192bfbb5 100644 --- a/src/authres.rs +++ b/src/authres.rs @@ -357,7 +357,6 @@ mod tests { use super::*; use crate::aheader::EncryptPreference; use crate::e2ee; - use crate::message; use crate::mimeparser; use crate::peerstate::Peerstate; use crate::securejoin::get_securejoin_qr; @@ -705,7 +704,7 @@ Authentication-Results: dkim="; let received = tcm .try_send_recv(&alice, &bob2, "My credit card number is 1234") .await; - assert!(!received.text.as_ref().unwrap().contains("1234")); + assert!(!received.text.contains("1234")); assert!(received.error.is_some()); tcm.section("Turns out bob2 wasn't an attacker at all, Bob just has a new phone and DKIM just stopped working."); @@ -786,7 +785,7 @@ Authentication-Results: dkim="; .insert_str(0, "List-Post: \n"); let rcvd = alice.recv_msg(&sent).await; assert!(!rcvd.get_showpadlock()); - assert_eq!(&rcvd.text.unwrap(), "hellooo in the mailinglist again"); + assert_eq!(&rcvd.text, "hellooo in the mailinglist again"); Ok(()) } @@ -825,7 +824,9 @@ Authentication-Results: dkim="; // Disallowing keychanges is disabled for now: // assert!(rcvd.error.unwrap().contains("DKIM failed")); // The message info should contain a warning: - assert!(message::get_msg_info(&bob, rcvd.id) + assert!(rcvd + .id + .get_info(&bob) .await .unwrap() .contains("KEYCHANGES NOT ALLOWED")); diff --git a/src/blob.rs b/src/blob.rs index c7a3641d2..40072cb53 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -9,21 +9,17 @@ use std::path::{Path, PathBuf}; use anyhow::{format_err, Context as _, Result}; use futures::StreamExt; -use image::{DynamicImage, ImageFormat}; +use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat}; use num_traits::FromPrimitive; use tokio::io::AsyncWriteExt; use tokio::{fs, io}; use tokio_stream::wrappers::ReadDirStream; use crate::config::Config; -use crate::constants::{ - MediaQuality, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE, WORSE_IMAGE_SIZE, -}; +use crate::constants::{self, MediaQuality}; use crate::context::Context; use crate::events::EventType; use crate::log::LogExt; -use crate::message; -use crate::message::Viewtype; /// Represents a file in the blob directory. /// @@ -323,119 +319,188 @@ impl<'a> BlobObject<'a> { match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?) .unwrap_or_default() { - MediaQuality::Balanced => BALANCED_AVATAR_SIZE, - MediaQuality::Worse => WORSE_AVATAR_SIZE, + MediaQuality::Balanced => constants::BALANCED_AVATAR_SIZE, + MediaQuality::Worse => constants::WORSE_AVATAR_SIZE, }; + let maybe_sticker = &mut false; + let strict_limits = true; // max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k. // 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k. - if let Some(new_name) = self.recode_to_size(context, blob_abs, img_wh, Some(20_000))? { + if let Some(new_name) = self.recode_to_size( + context, + blob_abs, + maybe_sticker, + img_wh, + 20_000, + strict_limits, + )? { self.name = new_name; } Ok(()) } - pub async fn recode_to_image_size(&self, context: &Context) -> Result<()> { + /// Recodes an image pointed by a [BlobObject] so that it fits into limits on the image width, + /// height and file size specified by the config. + /// + /// On some platforms images are passed to the core as [`crate::message::Viewtype::Sticker`] in + /// which case `maybe_sticker` flag should be set. We recheck if an image is a true sticker + /// assuming that it must have at least one fully transparent corner, otherwise this flag is + /// reset. + pub async fn recode_to_image_size( + &mut self, + context: &Context, + maybe_sticker: &mut bool, + ) -> Result<()> { let blob_abs = self.to_abs_path(); - if message::guess_msgtype_from_suffix(Path::new(&blob_abs)) - != Some((Viewtype::Image, "image/jpeg")) - { - return Ok(()); - } - - let img_wh = + let (img_wh, max_bytes) = match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?) .unwrap_or_default() { - MediaQuality::Balanced => BALANCED_IMAGE_SIZE, - MediaQuality::Worse => WORSE_IMAGE_SIZE, + MediaQuality::Balanced => ( + constants::BALANCED_IMAGE_SIZE, + constants::BALANCED_IMAGE_BYTES, + ), + MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES), }; - - if self - .recode_to_size(context, blob_abs, img_wh, None)? - .is_some() - { - return Err(format_err!( - "Internal error: recode_to_size(..., None) shouldn't change the name of the image" - )); + let strict_limits = false; + if let Some(new_name) = self.recode_to_size( + context, + blob_abs, + maybe_sticker, + img_wh, + max_bytes, + strict_limits, + )? { + self.name = new_name; } Ok(()) } + /// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just + /// proceed with the result. fn recode_to_size( - &self, + &mut self, context: &Context, mut blob_abs: PathBuf, + maybe_sticker: &mut bool, mut img_wh: u32, - max_bytes: Option, + max_bytes: usize, + strict_limits: bool, ) -> Result> { - tokio::task::block_in_place(move || { - let mut img = image::open(&blob_abs).context("image recode failure")?; - let orientation = self.get_exif_orientation(context); + let mut no_exif = false; + let no_exif_ref = &mut no_exif; + let res = tokio::task::block_in_place(move || { + let (nr_bytes, exif) = self.metadata()?; + *no_exif_ref = exif.is_none(); + let mut img = image::open(&blob_abs).context("image decode failure")?; + let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context)); let mut encoded = Vec::new(); let mut changed_name = None; - let exceeds_width = img.width() > img_wh || img.height() > img_wh; + if *maybe_sticker { + let x_max = img.width().saturating_sub(1); + let y_max = img.height().saturating_sub(1); + *maybe_sticker = img.in_bounds(x_max, y_max) + && (img.get_pixel(0, 0).0[3] == 0 + || img.get_pixel(x_max, 0).0[3] == 0 + || img.get_pixel(0, y_max).0[3] == 0 + || img.get_pixel(x_max, y_max).0[3] == 0); + } + if *maybe_sticker && exif.is_none() { + return Ok(None); + } - let do_scale = - exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?; - let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270)); + img = match orientation { + Some(90) => img.rotate90(), + Some(180) => img.rotate180(), + Some(270) => img.rotate270(), + _ => img, + }; - if do_scale || do_rotate { - if do_rotate { - img = match orientation { - Ok(90) => img.rotate90(), - Ok(180) => img.rotate180(), - Ok(270) => img.rotate270(), - _ => img, - } + let exceeds_wh = img.width() > img_wh || img.height() > img_wh; + let exceeds_max_bytes = nr_bytes > max_bytes as u64; + + let fmt = ImageFormat::from_path(&blob_abs); + let ofmt = match fmt { + Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png, + _ => { + let jpeg_quality = 75; + ImageOutputFormat::Jpeg(jpeg_quality) } - - if do_scale { - if !exceeds_width { - // The image is already smaller than img_wh, but exceeds max_bytes - // We can directly start with trying to scale down to 2/3 of its current width - img_wh = max(img.width(), img.height()) * 2 / 3 - } - - loop { - let new_img = img.thumbnail(img_wh, img_wh); - - if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? { - if img_wh < 20 { - return Err(format_err!( - "Failed to scale image to below {}B", - max_bytes.unwrap_or_default() - )); - } - - img_wh = img_wh * 2 / 3; - } else { - if encoded.is_empty() { - encode_img(&new_img, &mut encoded)?; - } - - info!( + }; + // We need to rewrite images with Exif to remove metadata such as location, + // camera model, etc. + // + // TODO: Fix lost animation and transparency when recoding using the `image` crate. And + // also `Viewtype::Gif` (maybe renamed to `Animation`) should be used for animated + // images. + let do_scale = exceeds_max_bytes + || strict_limits + && (exceeds_wh + || exif.is_some() + && encoded_img_exceeds_bytes( context, - "Final scaled-down image size: {}B ({}px).", - encoded.len(), - img_wh - ); - break; - } + &img, + ofmt.clone(), + max_bytes, + &mut encoded, + )?); + + if do_scale { + if !exceeds_wh { + img_wh = max(img.width(), img.height()); + // PNGs and WebPs may be huge because of animation, which is lost by the `image` + // crate when recoding, so don't scale them down. + if matches!(fmt, Ok(ImageFormat::Jpeg)) || !encoded.is_empty() { + img_wh = img_wh * 2 / 3; } } - // The file format is JPEG now, we may have to change the file extension - if !matches!(ImageFormat::from_path(&blob_abs), Ok(ImageFormat::Jpeg)) { + loop { + let new_img = img.thumbnail(img_wh, img_wh); + + if encoded_img_exceeds_bytes( + context, + &new_img, + ofmt.clone(), + max_bytes, + &mut encoded, + )? && strict_limits + { + if img_wh < 20 { + return Err(format_err!( + "Failed to scale image to below {}B.", + max_bytes, + )); + } + + img_wh = img_wh * 2 / 3; + } else { + info!( + context, + "Final scaled-down image size: {}B ({}px).", + encoded.len(), + img_wh + ); + break; + } + } + } + + if do_scale || exif.is_some() { + // The file format is JPEG/PNG now, we may have to change the file extension + if !matches!(fmt, Ok(ImageFormat::Jpeg)) + && matches!(ofmt, ImageOutputFormat::Jpeg(_)) + { blob_abs = blob_abs.with_extension("jpg"); - let file_name = blob_abs.file_name().context("No avatar file name (???)")?; + let file_name = blob_abs.file_name().context("No image file name (???)")?; let file_name = file_name.to_str().context("Filename is no UTF-8 (???)")?; changed_name = Some(format!("$BLOBDIR/{file_name}")); } if encoded.is_empty() { - encode_img(&img, &mut encoded)?; + encode_img(&img, ofmt, &mut encoded)?; } std::fs::write(&blob_abs, &encoded) @@ -443,26 +508,45 @@ impl<'a> BlobObject<'a> { } Ok(changed_name) - }) - } - - pub fn get_exif_orientation(&self, context: &Context) -> Result { - let file = std::fs::File::open(self.to_abs_path())?; - let mut bufreader = std::io::BufReader::new(&file); - let exifreader = exif::Reader::new(); - let exif = exifreader.read_from_container(&mut bufreader)?; - if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) { - // possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html - // we only use rotation, in practise, flipping is not used. - match orientation.value.get_uint(0) { - Some(3) => return Ok(180), - Some(6) => return Ok(90), - Some(8) => return Ok(270), - other => warn!(context, "Exif orientation value ignored: {other:?}."), + }); + match res { + Ok(_) => res, + Err(err) => { + if !strict_limits && no_exif { + warn!( + context, + "Cannot recode image, using original data: {err:#}.", + ); + Ok(None) + } else { + Err(err) + } } } - Ok(0) } + + /// Returns image file size and Exif. + pub fn metadata(&self) -> Result<(u64, Option)> { + let file = std::fs::File::open(self.to_abs_path())?; + let len = file.metadata()?.len(); + let mut bufreader = std::io::BufReader::new(&file); + let exif = exif::Reader::new().read_from_container(&mut bufreader).ok(); + Ok((len, exif)) + } +} + +fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 { + if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) { + // possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html + // we only use rotation, in practise, flipping is not used. + match orientation.value.get_uint(0) { + Some(3) => return 180, + Some(6) => return 90, + Some(8) => return 270, + other => warn!(context, "Exif orientation value ignored: {other:?}."), + } + } + 0 } impl<'a> fmt::Display for BlobObject<'a> { @@ -552,31 +636,35 @@ impl<'a> Iterator for BlobDirIter<'a> { impl FusedIterator for BlobDirIter<'_> {} -fn encode_img(img: &DynamicImage, encoded: &mut Vec) -> anyhow::Result<()> { +fn encode_img( + img: &DynamicImage, + fmt: ImageOutputFormat, + encoded: &mut Vec, +) -> anyhow::Result<()> { encoded.clear(); let mut buf = Cursor::new(encoded); - img.write_to(&mut buf, image::ImageFormat::Jpeg)?; + img.write_to(&mut buf, fmt)?; Ok(()) } + fn encoded_img_exceeds_bytes( context: &Context, img: &DynamicImage, - max_bytes: Option, + fmt: ImageOutputFormat, + max_bytes: usize, encoded: &mut Vec, ) -> anyhow::Result { - if let Some(max_bytes) = max_bytes { - encode_img(img, encoded)?; - if encoded.len() > max_bytes { - info!( - context, - "Image size {}B ({}x{}px) exceeds {}B, need to scale down.", - encoded.len(), - img.width(), - img.height(), - max_bytes, - ); - return Ok(true); - } + encode_img(img, fmt, encoded)?; + if encoded.len() > max_bytes { + info!( + context, + "Image size {}B ({}x{}px) exceeds {}B, need to scale down.", + encoded.len(), + img.width(), + img.height(), + max_bytes, + ); + return Ok(true); } Ok(false) } @@ -589,7 +677,7 @@ mod tests { use super::*; use crate::chat::{self, create_group_chat, ProtectionStatus}; - use crate::message::Message; + use crate::message::{Message, Viewtype}; use crate::test_utils::{self, TestContext}; fn check_image_size(path: impl AsRef, width: u32, height: u32) -> image::DynamicImage { @@ -814,17 +902,29 @@ mod tests { assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string())); check_image_size(avatar_src, 1000, 1000); - check_image_size(&avatar_blob, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE); + check_image_size( + &avatar_blob, + constants::BALANCED_AVATAR_SIZE, + constants::BALANCED_AVATAR_SIZE, + ); async fn file_size(path_buf: &Path) -> u64 { let file = File::open(path_buf).await.unwrap(); file.metadata().await.unwrap().len() } - let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap(); - - blob.recode_to_size(&t, blob.to_abs_path(), 1000, Some(3000)) - .unwrap(); + let mut blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap(); + let maybe_sticker = &mut false; + let strict_limits = true; + blob.recode_to_size( + &t, + blob.to_abs_path(), + maybe_sticker, + 1000, + 3000, + strict_limits, + ) + .unwrap(); assert!(file_size(&avatar_blob).await <= 3000); assert!(file_size(&avatar_blob).await > 2000); tokio::task::block_in_place(move || { @@ -850,10 +950,14 @@ mod tests { let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); assert_eq!( avatar_cfg, - avatar_src.with_extension("jpg").to_str().unwrap() + avatar_src.with_extension("png").to_str().unwrap() ); - check_image_size(avatar_cfg, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE); + check_image_size( + avatar_cfg, + constants::BALANCED_AVATAR_SIZE, + constants::BALANCED_AVATAR_SIZE, + ); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -879,18 +983,31 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_recode_image_1() { let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg"); - // BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down: - send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000) - .await - .unwrap(); send_image_check_mediaquality( - Some("1"), + Viewtype::Image, + Some("0"), bytes, + "jpg", + true, // has Exif 1000, 1000, 0, - WORSE_IMAGE_SIZE, - WORSE_IMAGE_SIZE, + 1000, + 1000, + ) + .await + .unwrap(); + send_image_check_mediaquality( + Viewtype::Image, + Some("1"), + bytes, + "jpg", + true, // has Exif + 1000, + 1000, + 0, + 1000, + 1000, ) .await .unwrap(); @@ -901,71 +1018,110 @@ mod tests { // The "-rotated" files are rotated by 270 degrees using the Exif metadata let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg"); let img_rotated = send_image_check_mediaquality( + Viewtype::Image, Some("0"), bytes, + "jpg", + true, // has Exif 2000, 1800, 270, - BALANCED_IMAGE_SIZE * 1800 / 2000, - BALANCED_IMAGE_SIZE, + 1800, + 2000, ) .await .unwrap(); assert_correct_rotation(&img_rotated); let mut buf = Cursor::new(vec![]); - img_rotated - .write_to(&mut buf, image::ImageFormat::Jpeg) - .unwrap(); + img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap(); let bytes = buf.into_inner(); - // Do this in parallel to speed up the test a bit - // (it still takes very long though) - let bytes2 = bytes.clone(); - let join_handle = tokio::task::spawn(async move { - let img_rotated = send_image_check_mediaquality( - Some("0"), - &bytes2, - BALANCED_IMAGE_SIZE * 1800 / 2000, - BALANCED_IMAGE_SIZE, - 0, - BALANCED_IMAGE_SIZE * 1800 / 2000, - BALANCED_IMAGE_SIZE, - ) - .await - .unwrap(); - assert_correct_rotation(&img_rotated); - }); - let img_rotated = send_image_check_mediaquality( + Viewtype::Image, Some("1"), &bytes, - BALANCED_IMAGE_SIZE * 1800 / 2000, - BALANCED_IMAGE_SIZE, + "jpg", + false, // no Exif + 1800, + 2000, 0, - WORSE_IMAGE_SIZE * 1800 / 2000, - WORSE_IMAGE_SIZE, + 1800, + 2000, ) .await .unwrap(); assert_correct_rotation(&img_rotated); - - join_handle.await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_recode_image_3() { - let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg"); - let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200) - .await - .unwrap(); - assert_correct_rotation(&img_rotated); + async fn test_recode_image_balanced_png() { + let bytes = include_bytes!("../test-data/image/screenshot.png"); - let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg"); - let img_rotated = send_image_check_mediaquality(Some("1"), bytes, 200, 180, 270, 180, 200) - .await - .unwrap(); - assert_correct_rotation(&img_rotated); + send_image_check_mediaquality( + Viewtype::Image, + Some("0"), + bytes, + "png", + false, // no Exif + 1920, + 1080, + 0, + 1920, + 1080, + ) + .await + .unwrap(); + + send_image_check_mediaquality( + Viewtype::Image, + Some("1"), + bytes, + "png", + false, // no Exif + 1920, + 1080, + 0, + constants::WORSE_IMAGE_SIZE, + constants::WORSE_IMAGE_SIZE * 1080 / 1920, + ) + .await + .unwrap(); + + // This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation. + send_image_check_mediaquality( + Viewtype::Sticker, + Some("0"), + bytes, + "png", + false, // no Exif + 1920, + 1080, + 0, + 1920, + 1080, + ) + .await + .unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_recode_image_huge_jpg() { + let bytes = include_bytes!("../test-data/image/screenshot.jpg"); + send_image_check_mediaquality( + Viewtype::Image, + Some("0"), + bytes, + "jpg", + true, // has Exif + 1920, + 1080, + 0, + constants::BALANCED_IMAGE_SIZE, + constants::BALANCED_IMAGE_SIZE * 1080 / 1920, + ) + .await + .unwrap(); } fn assert_correct_rotation(img: &DynamicImage) { @@ -985,9 +1141,13 @@ mod tests { assert_eq!(luma, 0); } + #[allow(clippy::too_many_arguments)] async fn send_image_check_mediaquality( + viewtype: Viewtype, media_quality_config: Option<&str>, bytes: &[u8], + extension: &str, + has_exif: bool, original_width: u32, original_height: u32, orientation: i32, @@ -999,7 +1159,7 @@ mod tests { alice .set_config(Config::MediaQuality, media_quality_config) .await?; - let file = alice.get_blobdir().join("file.jpg"); + let file = alice.get_blobdir().join("file").with_extension(extension); fs::write(&file, &bytes) .await @@ -1007,9 +1167,15 @@ mod tests { check_image_size(&file, original_width, original_height); let blob = BlobObject::new_from_path(&alice, &file).await?; - assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation); + let (_, exif) = blob.metadata()?; + if has_exif { + let exif = exif.unwrap(); + assert_eq!(exif_orientation(&exif, &alice), orientation); + } else { + assert!(exif.is_none()); + } - let mut msg = Message::new(Viewtype::Image); + let mut msg = Message::new(viewtype); msg.set_file(file.to_str().unwrap(), None); let chat = alice.create_chat(&bob).await; let sent = alice.send_msg(chat.id, &mut msg).await; @@ -1023,12 +1189,14 @@ mod tests { ); let bob_msg = bob.recv_msg(&sent).await; + assert_eq!(bob_msg.get_viewtype(), Viewtype::Image); assert_eq!(bob_msg.get_width() as u32, compressed_width); assert_eq!(bob_msg.get_height() as u32, compressed_height); let file = bob_msg.get_file(&bob).unwrap(); let blob = BlobObject::new_from_path(&bob, &file).await?; - assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0); + let (_, exif) = blob.metadata()?; + assert!(exif.is_none()); let img = check_image_size(file, compressed_width, compressed_height); Ok(img) diff --git a/src/chat.rs b/src/chat.rs index 7561c511f..b6d593822 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize}; use crate::aheader::EncryptPreference; use crate::blob::BlobObject; +use crate::chatlist::Chatlist; use crate::color::str_to_color; use crate::config::Config; use crate::constants::{ @@ -22,9 +23,11 @@ use crate::constants::{ use crate::contact::{Contact, ContactId, Origin, VerifiedStatus}; use crate::context::Context; use crate::debug_logging::maybe_set_logging_xdc; +use crate::download::DownloadState; use crate::ephemeral::Timer as EphemeralTimer; use crate::events::EventType; use crate::html::new_html_mimepart; +use crate::location; use crate::message::{self, Message, MessageState, MsgId, Viewtype}; use crate::mimefactory::MimeFactory; use crate::mimeparser::SystemMessage; @@ -33,6 +36,7 @@ use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; use crate::receive_imf::ReceivedMsg; use crate::scheduler::InterruptInfo; use crate::smtp::send_msg_to_smtp; +use crate::sql; use crate::stock_str; use crate::tools::{ buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp, @@ -40,7 +44,6 @@ use crate::tools::{ strip_rtlo_characters, time, IsNoneOrEmpty, }; use crate::webxdc::WEBXDC_SUFFIX; -use crate::{location, sql}; /// An chat item, such as a message or a marker. #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -86,6 +89,14 @@ pub enum ProtectionStatus { /// /// All members of the chat must be verified. Protected = 1, + + /// The chat was protected, but now a new message came in + /// which was not encrypted / signed correctly. + /// The user has to confirm that this is OK. + /// + /// We only do this in 1:1 chats; in group chats, the chat just + /// stays protected. + ProtectionBroken = 3, // `2` was never used as a value. } /// The reason why messages cannot be sent to the chat. @@ -102,6 +113,10 @@ pub(crate) enum CantSendReason { /// The chat is a contact request, it needs to be accepted before sending a message. ContactRequest, + /// The chat was protected, but now a new message came in + /// which was not encrypted / signed correctly. + ProtectionBroken, + /// Mailing list without known List-Post header. ReadOnlyMailingList, @@ -118,6 +133,10 @@ impl fmt::Display for CantSendReason { f, "contact request chat should be accepted before sending messages" ), + Self::ProtectionBroken => write!( + f, + "accept that the encryption isn't verified anymore before sending messages" + ), Self::ReadOnlyMailingList => { write!(f, "mailing list does not have a know post address") } @@ -270,6 +289,7 @@ impl ChatId { param: Option, ) -> Result { let grpname = strip_rtlo_characters(grpname); + let smeared_time = create_smeared_timestamp(context); let row_id = context.sql.insert( "INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);", @@ -278,13 +298,20 @@ impl ChatId { &grpname, grpid, create_blocked, - create_smeared_timestamp(context), + smeared_time, create_protected, param.unwrap_or_default(), ), ).await?; let chat_id = ChatId::new(u32::try_from(row_id)?); + + if create_protected == ProtectionStatus::Protected { + chat_id + .add_protection_msg(context, ProtectionStatus::Protected, None, smeared_time) + .await?; + } + info!( context, "Created group/mailinglist '{}' grpid={} as {}, blocked={}.", @@ -332,7 +359,7 @@ impl ChatId { let chat = Chat::load_from_db(context, self).await?; match chat.typ { - Chattype::Undefined | Chattype::Broadcast => { + Chattype::Broadcast => { bail!("Can't block chat of type {:?}", chat.typ) } Chattype::Single => { @@ -373,7 +400,16 @@ impl ChatId { let chat = Chat::load_from_db(context, self).await?; match chat.typ { - Chattype::Undefined => bail!("Can't accept chat of undefined chattype"), + Chattype::Single + if chat.blocked == Blocked::Not + && chat.protected == ProtectionStatus::ProtectionBroken => + { + // The protection was broken, then the user clicked 'Accept'/'OK', + // so, now we want to set the status to Unprotected again: + chat.id + .inner_set_protection(context, ProtectionStatus::Unprotected) + .await?; + } Chattype::Single | Chattype::Group | Chattype::Broadcast => { // User has "created a chat" with all these contacts. // @@ -400,20 +436,19 @@ impl ChatId { /// Sets protection without sending a message. /// - /// Used when a message arrives indicating that someone else has - /// changed the protection value for a chat. + /// Returns whether the protection status was actually modified. pub(crate) async fn inner_set_protection( self, context: &Context, protect: ProtectionStatus, - ) -> Result<()> { - ensure!(!self.is_special(), "Invalid chat-id."); + ) -> Result { + ensure!(!self.is_special(), "Invalid chat-id {self}."); let chat = Chat::load_from_db(context, self).await?; if protect == chat.protected { info!(context, "Protection status unchanged for {}.", self); - return Ok(()); + return Ok(false); } match protect { @@ -428,9 +463,8 @@ impl ChatId { } } Chattype::Mailinglist => bail!("Cannot protect mailing lists"), - Chattype::Undefined => bail!("Undefined group type"), }, - ProtectionStatus::Unprotected => {} + ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {} }; context @@ -443,68 +477,58 @@ impl ChatId { // make sure, the receivers will get all keys self.reset_gossiped_timestamp(context).await?; - Ok(()) + Ok(true) } - /// Send protected status message to the chat. + /// Adds an info message to the chat, telling the user that the protection status changed. /// - /// This sends the message with the protected status change to the chat, - /// notifying the user on this device as well as the other users in the chat. + /// Params: /// - /// If `promote` is false this means, the message must not be sent out - /// and only a local info message should be added to the chat. - /// This is used when protection is enabled implicitly or when a chat is not yet promoted. + /// * `contact_id`: In a 1:1 chat, pass the chat partner's contact id. + /// * `timestamp_sort` is used as the timestamp of the added message + /// and should be the timestamp of the change happening. pub(crate) async fn add_protection_msg( self, context: &Context, protect: ProtectionStatus, - promote: bool, - from_id: ContactId, + contact_id: Option, + timestamp_sort: i64, ) -> Result<()> { - let msg_text = context.stock_protection_msg(protect, from_id).await; + let text = context.stock_protection_msg(protect, contact_id).await; let cmd = match protect { ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled, ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled, + ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled, }; - - if promote { - let mut msg = Message { - viewtype: Viewtype::Text, - text: Some(msg_text), - ..Default::default() - }; - msg.param.set_cmd(cmd); - send_msg(context, self, &mut msg).await?; - } else { - add_info_msg_with_cmd( - context, - self, - &msg_text, - cmd, - create_smeared_timestamp(context), - None, - None, - None, - ) - .await?; - } + add_info_msg_with_cmd(context, self, &text, cmd, timestamp_sort, None, None, None).await?; Ok(()) } /// Sets protection and sends or adds a message. - pub async fn set_protection(self, context: &Context, protect: ProtectionStatus) -> Result<()> { - ensure!(!self.is_special(), "set protection: invalid chat-id."); - - let chat = Chat::load_from_db(context, self).await?; - - if let Err(e) = self.inner_set_protection(context, protect).await { - error!(context, "Cannot set protection: {e:#}."); // make error user-visible - return Err(e); + /// + /// `timestamp_sort` is used as the timestamp of the added message + /// and should be the timestamp of the change happening. + pub(crate) async fn set_protection( + self, + context: &Context, + protect: ProtectionStatus, + timestamp_sort: i64, + contact_id: Option, + ) -> Result<()> { + match self.inner_set_protection(context, protect).await { + Ok(protection_status_modified) => { + if protection_status_modified { + self.add_protection_msg(context, protect, contact_id, timestamp_sort) + .await?; + } + Ok(()) + } + Err(e) => { + error!(context, "Cannot set protection: {e:#}."); // make error user-visible + Err(e) + } } - - self.add_protection_msg(context, protect, chat.is_promoted(), ContactId::SELF) - .await } /// Archives or unarchives a chat. @@ -642,7 +666,7 @@ impl ChatId { if chat.is_self_talk() { let mut msg = Message::new(Viewtype::Text); - msg.text = Some(stock_str::self_deleted_msg_body(context).await); + msg.text = stock_str::self_deleted_msg_body(context).await; add_device_msg(context, None, Some(&mut msg)).await?; } @@ -724,7 +748,7 @@ impl ChatId { match msg.viewtype { Viewtype::Unknown => bail!("Can not set draft of unknown type."), Viewtype::Text => { - if msg.text.is_none_or_empty() && msg.in_reply_to.is_none_or_empty() { + if msg.text.is_empty() && msg.in_reply_to.is_none_or_empty() { bail!("No text and no quote in draft"); } } @@ -741,14 +765,6 @@ impl ChatId { } } - let chat = Chat::load_from_db(context, self).await?; - if let Some(cant_send_reason) = chat.why_cant_send(context).await? { - bail!( - "Can't set a draft because chat is not writeable: {}", - cant_send_reason - ); - } - // set back draft information to allow identifying the draft later on - // no matter if message object is reused or reloaded from db msg.state = MessageState::OutDraft; @@ -770,7 +786,7 @@ impl ChatId { ( time(), msg.viewtype, - msg.text.as_deref().unwrap_or(""), + &msg.text, msg.param.to_string(), msg.in_reply_to.as_deref().unwrap_or_default(), msg.id, @@ -804,7 +820,7 @@ impl ChatId { time(), msg.viewtype, MessageState::OutDraft, - msg.text.as_deref().unwrap_or(""), + &msg.text, msg.param.to_string(), 1, msg.in_reply_to.as_deref().unwrap_or_default(), @@ -871,6 +887,134 @@ impl ChatId { Ok(count) } + /// Returns timestamp of the latest message in the chat. + pub(crate) async fn get_timestamp(self, context: &Context) -> Result> { + let timestamp = context + .sql + .query_get_value("SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", (self,)) + .await?; + Ok(timestamp) + } + + /// Returns a list of active similar chat IDs sorted by similarity metric. + /// + /// Jaccard similarity coefficient is used to estimate similarity of chat member sets. + /// + /// Chat is considered active if something was posted there within the last 42 days. + pub async fn get_similar_chat_ids(self, context: &Context) -> Result> { + // Count number of common members in this and other chats. + let intersection: Vec<(ChatId, f64)> = context + .sql + .query_map( + "SELECT y.chat_id, SUM(x.contact_id = y.contact_id) + FROM chats_contacts as x + JOIN chats_contacts as y + WHERE x.contact_id > 9 + AND y.contact_id > 9 + AND x.chat_id=? + AND y.chat_id<>x.chat_id + GROUP BY y.chat_id", + (self,), + |row| { + let chat_id: ChatId = row.get(0)?; + let intersection: f64 = row.get(1)?; + Ok((chat_id, intersection)) + }, + |rows| { + rows.collect::, _>>() + .map_err(Into::into) + }, + ) + .await + .context("failed to calculate member set intersections")?; + + let chat_size: HashMap = context + .sql + .query_map( + "SELECT chat_id, count(*) AS n + FROM chats_contacts + WHERE contact_id > ? AND chat_id > ? + GROUP BY chat_id", + (ContactId::LAST_SPECIAL, DC_CHAT_ID_LAST_SPECIAL), + |row| { + let chat_id: ChatId = row.get(0)?; + let size: f64 = row.get(1)?; + Ok((chat_id, size)) + }, + |rows| { + rows.collect::, _>>() + .map_err(Into::into) + }, + ) + .await + .context("failed to count chat member sizes")?; + + let our_chat_size = chat_size.get(&self).copied().unwrap_or_default(); + let mut chats_with_metrics = Vec::new(); + for (chat_id, intersection_size) in intersection { + if intersection_size > 0.0 { + let other_chat_size = chat_size.get(&chat_id).copied().unwrap_or_default(); + let union_size = our_chat_size + other_chat_size - intersection_size; + let metric = intersection_size / union_size; + chats_with_metrics.push((chat_id, metric)) + } + } + chats_with_metrics.sort_unstable_by(|(chat_id1, metric1), (chat_id2, metric2)| { + metric2 + .partial_cmp(metric1) + .unwrap_or(chat_id2.cmp(chat_id1)) + }); + + // Select up to five similar active chats. + let mut res = Vec::new(); + let now = time(); + for (chat_id, metric) in chats_with_metrics { + if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? { + if now > chat_timestamp + 42 * 24 * 3600 { + // Chat was inactive for 42 days, skip. + continue; + } + } + + if metric < 0.1 { + // Chat is unrelated. + break; + } + + let chat = Chat::load_from_db(context, chat_id).await?; + if chat.typ != Chattype::Group { + continue; + } + + match chat.visibility { + ChatVisibility::Normal | ChatVisibility::Pinned => {} + ChatVisibility::Archived => continue, + } + + res.push((chat_id, metric)); + if res.len() >= 5 { + break; + } + } + + Ok(res) + } + + /// Returns similar chats as a [`Chatlist`]. + /// + /// [`Chatlist`]: crate::chatlist::Chatlist + pub async fn get_similar_chatlist(self, context: &Context) -> Result { + let chat_ids: Vec = self + .get_similar_chat_ids(context) + .await + .context("failed to get similar chat IDs")? + .into_iter() + .map(|(chat_id, _metric)| chat_id) + .collect(); + let chatlist = Chatlist::from_chat_ids(context, &chat_ids).await?; + Ok(chatlist) + } + pub(crate) async fn get_param(self, context: &Context) -> Result { let res: Option = context .sql @@ -910,11 +1054,14 @@ impl ChatId { T: Send + 'static, { let sql = &context.sql; + // Do not reply to not fully downloaded messages. Such a message could be a group chat + // message that we assigned to 1:1 chat. let query = format!( "SELECT {fields} \ - FROM msgs WHERE chat_id=? AND state NOT IN (?, ?, ?, ?) AND NOT hidden \ + FROM msgs WHERE chat_id=? AND state NOT IN (?, ?) AND NOT hidden AND download_state={} \ ORDER BY timestamp DESC, id DESC \ - LIMIT 1;" + LIMIT 1;", + DownloadState::Done as u32, ); let row = sql .query_row_optional( @@ -923,8 +1070,11 @@ impl ChatId { self, MessageState::OutPreparing, MessageState::OutDraft, - MessageState::OutPending, - MessageState::OutFailed, + // We don't filter `OutPending` and `OutFailed` messages because the new message + // for which `parent_query()` is done may assume that it will be received in a + // context affected by those messages, e.g. they could add new members to a + // group and the new message will contain them in "To:". Anyway recipients must + // be prepared to orphaned references. ), f, ) @@ -936,34 +1086,17 @@ impl ChatId { self, context: &Context, ) -> Result> { - if let Some((rfc724_mid, mime_in_reply_to, mime_references, error)) = self - .parent_query( - context, - "rfc724_mid, mime_in_reply_to, mime_references, error", - |row: &rusqlite::Row| { - let rfc724_mid: String = row.get(0)?; - let mime_in_reply_to: String = row.get(1)?; - let mime_references: String = row.get(2)?; - let error: String = row.get(3)?; - Ok((rfc724_mid, mime_in_reply_to, mime_references, error)) - }, - ) - .await? - { - if !error.is_empty() { - // Do not reply to error messages. - // - // An error message could be a group chat message that we failed to decrypt and - // assigned to 1:1 chat. A reply to it will show up as a reply to group message - // on the other side. To avoid such situations, it is better not to reply to - // error messages at all. - Ok(None) - } else { - Ok(Some((rfc724_mid, mime_in_reply_to, mime_references))) - } - } else { - Ok(None) - } + self.parent_query( + context, + "rfc724_mid, mime_in_reply_to, mime_references", + |row: &rusqlite::Row| { + let rfc724_mid: String = row.get(0)?; + let mime_in_reply_to: String = row.get(1)?; + let mime_references: String = row.get(2)?; + Ok((rfc724_mid, mime_in_reply_to, mime_references)) + }, + ) + .await } /// Returns multi-line text summary of encryption preferences of all chat contacts. @@ -983,7 +1116,7 @@ impl ChatId { .iter() .filter(|&contact_id| !contact_id.is_special()) { - let contact = Contact::load_from_db(context, *contact_id).await?; + let contact = Contact::get_by_id(context, *contact_id).await?; let addr = contact.get_addr(); let peerstate = Peerstate::from_addr(context, addr).await?; @@ -1141,7 +1274,7 @@ pub struct Chat { pub grpid: String, /// Whether the chat is blocked, unblocked or a contact request. - pub(crate) blocked: Blocked, + pub blocked: Blocked, /// Additional chat parameters stored in the database. pub param: Params, @@ -1153,7 +1286,7 @@ pub struct Chat { pub mute_duration: MuteDuration, /// If the chat is protected (verified). - protected: ProtectionStatus, + pub(crate) protected: ProtectionStatus, } impl Chat { @@ -1241,13 +1374,16 @@ impl Chat { pub(crate) async fn why_cant_send(&self, context: &Context) -> Result> { use CantSendReason::*; + // NB: Don't forget to update Chatlist::try_load() when changing this function! let reason = if self.id.is_special() { Some(SpecialChat) } else if self.is_device_talk() { Some(DeviceChat) } else if self.is_contact_request() { Some(ContactRequest) - } else if self.is_mailing_list() && self.param.get(Param::ListPost).is_none_or_empty() { + } else if self.is_protection_broken() { + Some(ProtectionBroken) + } else if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() { Some(ReadOnlyMailingList) } else if !self.is_self_in_chat(context).await? { Some(NotAMember) @@ -1271,7 +1407,6 @@ impl Chat { match self.typ { Chattype::Single | Chattype::Broadcast | Chattype::Mailinglist => Ok(true), Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await, - Chattype::Undefined => Ok(false), } } @@ -1310,11 +1445,11 @@ impl Chat { pub async fn get_profile_image(&self, context: &Context) -> Result> { if let Some(image_rel) = self.param.get(Param::ProfileImage) { if !image_rel.is_empty() { - return Ok(Some(get_abs_path(context, image_rel))); + return Ok(Some(get_abs_path(context, Path::new(&image_rel)))); } } else if self.id.is_archived_link() { if let Ok(image_rel) = get_archive_icon(context).await { - return Ok(Some(get_abs_path(context, image_rel))); + return Ok(Some(get_abs_path(context, Path::new(&image_rel)))); } } else if self.typ == Chattype::Single { let contacts = get_chat_contacts(context, self.id).await?; @@ -1325,7 +1460,7 @@ impl Chat { } } else if self.typ == Chattype::Broadcast { if let Ok(image_rel) = get_broadcast_icon(context).await { - return Ok(Some(get_abs_path(context, image_rel))); + return Ok(Some(get_abs_path(context, Path::new(&image_rel)))); } } Ok(None) @@ -1358,7 +1493,7 @@ impl Chat { /// deltachat, and the data returned is still subject to change. pub async fn get_info(&self, context: &Context) -> Result { let draft = match self.id.get_draft(context).await? { - Some(message) => message.text.unwrap_or_default(), + Some(message) => message.text, _ => String::new(), }; Ok(ChatInfo { @@ -1400,6 +1535,7 @@ impl Chat { } /// Returns true if the chat is promoted. + /// This means a message has been sent to it and it _not_ only exists on the users device. pub fn is_promoted(&self) -> bool { !self.is_unpromoted() } @@ -1409,6 +1545,27 @@ impl Chat { self.protected == ProtectionStatus::Protected } + /// Returns true if the chat was protected, and then an incoming message broke this protection. + /// + /// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag, + /// otherwise it will return false for all chats. + /// + /// 1:1 chats are automatically set as protected when a contact is verified. + /// When a message comes in that is not encrypted / signed correctly, + /// the chat is automatically set as unprotected again. + /// `is_protection_broken()` will return true until `chat_id.accept()` is called. + /// + /// The UI should let the user confirm that this is OK with a message like + /// `Bob sent a message from another device. Tap to learn more` + /// and then call `chat_id.accept()`. + pub fn is_protection_broken(&self) -> bool { + match self.protected { + ProtectionStatus::Protected => false, + ProtectionStatus::Unprotected => false, + ProtectionStatus::ProtectionBroken => true, + } + } + /// Returns true if location streaming is enabled in the chat. pub fn is_sending_locations(&self) -> bool { self.is_sending_locations @@ -1439,15 +1596,6 @@ impl Chat { let mut to_id = 0; let mut location_id = 0; - if let Some(reason) = self.why_cant_send(context).await? { - if self.typ == Chattype::Group && reason == CantSendReason::NotAMember { - context.emit_event(EventType::ErrorSelfNotInGroup( - "Cannot send message; self not in group.".into(), - )); - } - bail!("Cannot send message to {}: {}", self.id, reason); - } - let from = context.get_primary_self_addr().await?; let new_rfc724_mid = { let grpid = match self.typ { @@ -1583,6 +1731,11 @@ impl Chat { None }; + msg.chat_id = self.id; + msg.from_id = ContactId::SELF; + msg.rfc724_mid = new_rfc724_mid; + msg.timestamp_sort = timestamp; + // add message to the database if let Some(update_msg_id) = update_msg_id { context @@ -1596,14 +1749,14 @@ impl Chat { ephemeral_timestamp=? WHERE id=?;", params_slice![ - new_rfc724_mid, - self.id, - ContactId::SELF, + msg.rfc724_mid, + msg.chat_id, + msg.from_id, to_id, - timestamp, + msg.timestamp_sort, msg.viewtype, msg.state, - msg.text.as_ref().cloned().unwrap_or_default(), + msg.text, &msg.subject, msg.param.to_string(), msg.hidden, @@ -1645,14 +1798,14 @@ impl Chat { ephemeral_timestamp) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);", params_slice![ - new_rfc724_mid, - self.id, - ContactId::SELF, + msg.rfc724_mid, + msg.chat_id, + msg.from_id, to_id, - timestamp, + msg.timestamp_sort, msg.viewtype, msg.state, - msg.text.as_ref().cloned().unwrap_or_default(), + msg.text, &msg.subject, msg.param.to_string(), msg.hidden, @@ -1808,7 +1961,7 @@ pub(crate) async fn update_device_icon(context: &Context) -> Result<()> { chat.param.set(Param::ProfileImage, &icon); chat.update_param(context).await?; - let mut contact = Contact::load_from_db(context, ContactId::DEVICE).await?; + let mut contact = Contact::get_by_id(context, ContactId::DEVICE).await?; contact.param.set(Param::ProfileImage, icon); contact.update_param(context).await?; } @@ -1932,7 +2085,7 @@ impl ChatIdBlocked { /// Returns the chat for the 1:1 chat with this contact. /// - /// I the chat does not yet exist a new one is created, using the provided [`Blocked`] + /// If the chat does not yet exist a new one is created, using the provided [`Blocked`] /// state. pub async fn get_for_contact( context: &Context, @@ -1950,7 +2103,7 @@ impl ChatIdBlocked { return Ok(res); } - let contact = Contact::load_from_db(context, contact_id).await?; + let contact = Contact::get_by_id(context, contact_id).await?; let chat_name = contact.get_display_name().to_string(); let mut params = Params::new(); match contact_id { @@ -1963,19 +2116,30 @@ impl ChatIdBlocked { _ => (), } + let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?; + let protected = peerstate.map_or(false, |p| { + p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual + }); + let smeared_time = create_smeared_timestamp(context); + let chat_id = context .sql .transaction(move |transaction| { transaction.execute( "INSERT INTO chats - (type, name, param, blocked, created_timestamp) - VALUES(?, ?, ?, ?, ?)", + (type, name, param, blocked, created_timestamp, protected) + VALUES(?, ?, ?, ?, ?, ?)", ( Chattype::Single, chat_name, params.to_string(), create_blocked as u8, - create_smeared_timestamp(context), + smeared_time, + if protected { + ProtectionStatus::Protected + } else { + ProtectionStatus::Unprotected + }, ), )?; let chat_id = ChatId::new( @@ -1996,6 +2160,17 @@ impl ChatIdBlocked { }) .await?; + if protected { + chat_id + .add_protection_msg( + context, + ProtectionStatus::Protected, + Some(contact_id), + smeared_time, + ) + .await?; + } + match contact_id { ContactId::SELF => update_saved_messages_icon(context).await?, ContactId::DEVICE => update_device_icon(context).await?, @@ -2026,21 +2201,32 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> { if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation { // the caller should check if the message text is empty } else if msg.viewtype.has_file() { - let blob = msg + let mut blob = msg .param .get_blob(Param::File, context, !msg.is_increation()) .await? .with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?; - if msg.viewtype == Viewtype::Image { - if let Err(err) = blob.recode_to_image_size(context).await { - warn!( - context, - "Cannot recode image, using original data: {err:#}." - ); + let mut maybe_sticker = msg.viewtype == Viewtype::Sticker; + if msg.viewtype == Viewtype::Image + || maybe_sticker && !msg.param.exists(Param::ForceSticker) + { + blob.recode_to_image_size(context, &mut maybe_sticker) + .await?; + + if !maybe_sticker { + msg.viewtype = Viewtype::Image; } } msg.param.set(Param::File, blob.as_name()); + if let (Some(filename), Some(blob_ext)) = (msg.param.get(Param::Filename), blob.suffix()) { + let stem = match filename.rsplit_once('.') { + Some((stem, _)) => stem, + None => filename, + }; + msg.param + .set(Param::Filename, stem.to_string() + "." + blob_ext); + } if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image { // Correct the type, take care not to correct already very special @@ -2076,6 +2262,8 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> { } } + msg.try_calc_and_set_dimensions(context).await?; + info!( context, "Attaching \"{}\" for message type #{}.", @@ -2099,7 +2287,13 @@ async fn prepare_msg_common( // Check if the chat can be sent to. if let Some(reason) = chat.why_cant_send(context).await? { - bail!("cannot send to {}: {}", chat_id, reason); + if reason == CantSendReason::ProtectionBroken + && msg.param.get_cmd() == SystemMessage::SecurejoinMessage + { + // Send out the message, the securejoin message is supposed to repair the verification + } else { + bail!("cannot send to {chat_id}: {reason}"); + } } // check current MessageState for drafts (to keep msg_id) ... @@ -2201,11 +2395,9 @@ pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message } async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result { - // protect all system messages againts RTLO attacks + // protect all system messages against RTLO attacks if msg.is_system_message() { - if let Some(text) = &msg.text { - msg.text = Some(strip_rtlo_characters(text.as_ref())); - } + msg.text = strip_rtlo_characters(&msg.text); } if prepare_send_msg(context, chat_id, msg).await?.is_some() { @@ -2245,7 +2437,7 @@ async fn prepare_send_msg( ); message::update_msg_state(context, msg.id, MessageState::OutPending).await?; } - let row_id = create_send_msg_job(context, msg.id).await?; + let row_id = create_send_msg_job(context, msg).await?; Ok(row_id) } @@ -2255,13 +2447,10 @@ async fn prepare_send_msg( /// group with only self and no BCC-to-self configured. /// /// The caller has to interrupt SMTP loop or otherwise process a new row. -async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result> { - let mut msg = Message::load_from_db(context, msg_id).await?; - msg.try_calc_and_set_dimensions(context) - .await - .context("failed to calculate media dimensions")?; - - /* create message */ +pub(crate) async fn create_send_msg_job( + context: &Context, + msg: &mut Message, +) -> Result> { let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default(); let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await { @@ -2272,7 +2461,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result Result Ok(res), Err(err) => { - message::set_msg_failed(context, msg_id, &err.to_string()).await; + message::set_msg_failed(context, msg, &err.to_string()).await?; Err(err) } }?; @@ -2312,13 +2502,13 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result Result Result Re let mut msg = Message::new(Viewtype::VideochatInvitation); msg.param.set(Param::WebrtcRoom, &instance); - msg.text = Some( + msg.text = stock_str::videochat_invite_msg_body(context, &Message::parse_webrtc_instance(&instance).1) - .await, - ); + .await; send_msg(context, chat_id, &mut msg).await } @@ -2568,14 +2756,7 @@ pub(crate) async fn marknoticed_chat_if_older_than( chat_id: ChatId, timestamp: i64, ) -> Result<()> { - if let Some(chat_timestamp) = context - .sql - .query_get_value( - "SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", - (chat_id,), - ) - .await? - { + if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? { if timestamp > chat_timestamp { marknoticed_chat(context, chat_id).await?; } @@ -2774,6 +2955,9 @@ pub enum Direction { } /// Searches next/previous message based on the given message and list of types. +/// +/// Deprecated since 2023-10-03. +#[deprecated(note = "use `get_chat_media` instead")] pub async fn get_next_media( context: &Context, curr_msg_id: MsgId, @@ -2852,18 +3036,14 @@ pub async fn create_group_chat( let grpid = create_id(); + let timestamp = create_smeared_timestamp(context); let row_id = context .sql .insert( "INSERT INTO chats (type, name, grpid, param, created_timestamp) VALUES(?, ?, ?, \'U=1\', ?);", - ( - Chattype::Group, - chat_name, - grpid, - create_smeared_timestamp(context), - ), + (Chattype::Group, chat_name, grpid, timestamp), ) .await?; @@ -2875,9 +3055,9 @@ pub async fn create_group_chat( context.emit_msgs_changed_without_ids(); if protect == ProtectionStatus::Protected { - // this part is to stay compatible to verified groups, - // in some future, we will drop the "protect"-flag from create_group_chat() - chat_id.inner_set_protection(context, protect).await?; + chat_id + .set_protection(context, protect, timestamp, None) + .await?; } Ok(chat_id) @@ -2969,6 +3149,7 @@ pub(crate) async fn remove_from_chat_contacts_table( } /// Adds a contact to the chat. +/// If the group is promoted, also sends out a system message to all group members pub async fn add_contact_to_chat( context: &Context, chat_id: ChatId, @@ -2990,7 +3171,7 @@ pub(crate) async fn add_contact_to_chat_ex( chat_id.reset_gossiped_timestamp(context).await?; - /*this also makes sure, not contacts are added to special or normal chats*/ + // this also makes sure, no contacts are added to special or normal chats let mut chat = Chat::load_from_db(context, chat_id).await?; ensure!( chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast, @@ -3012,7 +3193,7 @@ pub(crate) async fn add_contact_to_chat_ex( context.emit_event(EventType::ErrorSelfNotInGroup( "Cannot add contact to group; self not in group.".into(), )); - bail!("can not add contact because our account is not part of it"); + bail!("can not add contact because the account is not part of the group/broadcast"); } if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 { @@ -3055,10 +3236,10 @@ pub(crate) async fn add_contact_to_chat_ex( if chat.typ == Chattype::Group && chat.is_promoted() { msg.viewtype = Viewtype::Text; - msg.text = - Some(stock_str::msg_add_member(context, contact.get_addr(), ContactId::SELF).await); + let contact_addr = contact.get_addr(); + msg.text = stock_str::msg_add_member_local(context, contact_addr, ContactId::SELF).await; msg.param.set_cmd(SystemMessage::MemberAddedToGroup); - msg.param.set(Param::Arg, contact.get_addr()); + msg.param.set(Param::Arg, contact_addr); msg.param.set_int(Param::Arg2, from_handshake.into()); msg.id = send_msg(context, chat_id, &mut msg).await?; } @@ -3178,59 +3359,53 @@ pub async fn remove_contact_from_chat( ); let mut msg = Message::default(); - let mut success = false; - /* we do not check if "contact_id" exists but just delete all records with the id from chats_contacts */ - /* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */ - if let Ok(chat) = Chat::load_from_db(context, chat_id).await { - if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast { - if !chat.is_self_in_chat(context).await? { - context.emit_event(EventType::ErrorSelfNotInGroup( - "Cannot remove contact from chat; self not in group.".into(), - )); - } else { - if let Ok(contact) = Contact::get_by_id(context, contact_id).await { - if chat.typ == Chattype::Group && chat.is_promoted() { - msg.viewtype = Viewtype::Text; - if contact.id == ContactId::SELF { - set_group_explicitly_left(context, &chat.grpid).await?; - msg.text = - Some(stock_str::msg_group_left(context, ContactId::SELF).await); - } else { - msg.text = Some( - stock_str::msg_del_member( - context, - contact.get_addr(), - ContactId::SELF, - ) - .await, - ); - } - msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup); - msg.param.set(Param::Arg, contact.get_addr()); - msg.id = send_msg(context, chat_id, &mut msg).await?; + let chat = Chat::load_from_db(context, chat_id).await?; + if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast { + if !chat.is_self_in_chat(context).await? { + let err_msg = format!( + "Cannot remove contact {contact_id} from chat {chat_id}: self not in group." + ); + context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone())); + bail!("{}", err_msg); + } else { + // We do not return an error if the contact does not exist in the database. + // This allows to delete dangling references to deleted contacts + // in case of the database becoming inconsistent due to a bug. + if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? { + if chat.typ == Chattype::Group && chat.is_promoted() { + msg.viewtype = Viewtype::Text; + if contact.id == ContactId::SELF { + set_group_explicitly_left(context, &chat.grpid).await?; + msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await; + } else { + msg.text = stock_str::msg_del_member_local( + context, + contact.get_addr(), + ContactId::SELF, + ) + .await; } + msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup); + msg.param.set(Param::Arg, contact.get_addr()); + msg.id = send_msg(context, chat_id, &mut msg).await?; } - // we remove the member from the chat after constructing the - // to-be-send message. If between send_msg() and here the - // process dies the user will have to re-do the action. It's - // better than the other way round: you removed - // someone from DB but no peer or device gets to know about it and - // group membership is thus different on different devices. - // Note also that sending a message needs all recipients - // in order to correctly determine encryption so if we - // removed it first, it would complicate the - // check/encryption logic. - success = remove_from_chat_contacts_table(context, chat_id, contact_id) - .await - .is_ok(); - context.emit_event(EventType::ChatModified(chat_id)); } + // we remove the member from the chat after constructing the + // to-be-send message. If between send_msg() and here the + // process dies the user will have to re-do the action. It's + // better than the other way round: you removed + // someone from DB but no peer or device gets to know about it and + // group membership is thus different on different devices. + // Note also that sending a message needs all recipients + // in order to correctly determine encryption so if we + // removed it first, it would complicate the + // check/encryption logic. + remove_from_chat_contacts_table(context, chat_id, contact_id).await?; + context.emit_event(EventType::ChatModified(chat_id)); } - } - - if !success { - bail!("Failed to remove contact"); + } else { + bail!("Cannot remove members from non-group chats."); } Ok(()) @@ -3291,9 +3466,8 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) - && improve_single_line_input(&chat.name) != new_name { msg.viewtype = Viewtype::Text; - msg.text = Some( - stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await, - ); + msg.text = + stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await; msg.param.set_cmd(SystemMessage::GroupNameChanged); if !chat.name.is_empty() { msg.param.set(Param::Arg, &chat.name); @@ -3342,13 +3516,13 @@ pub async fn set_chat_profile_image( if new_image.is_empty() { chat.param.remove(Param::ProfileImage); msg.param.remove(Param::Arg); - msg.text = Some(stock_str::msg_grp_img_deleted(context, ContactId::SELF).await); + msg.text = stock_str::msg_grp_img_deleted(context, ContactId::SELF).await; } else { let mut image_blob = BlobObject::new_from_path(context, Path::new(new_image)).await?; image_blob.recode_to_avatar_size(context).await?; chat.param.set(Param::ProfileImage, image_blob.as_name()); msg.param.set(Param::Arg, image_blob.as_name()); - msg.text = Some(stock_str::msg_grp_img_changed(context, ContactId::SELF).await); + msg.text = stock_str::msg_grp_img_changed(context, ContactId::SELF).await; } chat.update_param(context).await?; if chat.is_promoted() && !chat.is_mailing_list() { @@ -3371,89 +3545,88 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) chat_id .unarchive_if_not_muted(context, MessageState::Undefined) .await?; - if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await { - if let Some(reason) = chat.why_cant_send(context).await? { - bail!("cannot send to {}: {}", chat_id, reason); + let mut chat = Chat::load_from_db(context, chat_id).await?; + if let Some(reason) = chat.why_cant_send(context).await? { + bail!("cannot send to {}: {}", chat_id, reason); + } + curr_timestamp = create_smeared_timestamps(context, msg_ids.len()); + let ids = context + .sql + .query_map( + &format!( + "SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id", + sql::repeat_vars(msg_ids.len()) + ), + rusqlite::params_from_iter(msg_ids), + |row| row.get::<_, MsgId>(0), + |ids| ids.collect::, _>>().map_err(Into::into), + ) + .await?; + + for id in ids { + let src_msg_id: MsgId = id; + let mut msg = Message::load_from_db(context, src_msg_id).await?; + if msg.state == MessageState::OutDraft { + bail!("cannot forward drafts."); } - curr_timestamp = create_smeared_timestamps(context, msg_ids.len()); - let ids = context - .sql - .query_map( - &format!( - "SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id", - sql::repeat_vars(msg_ids.len()) - ), - rusqlite::params_from_iter(msg_ids), - |row| row.get::<_, MsgId>(0), - |ids| ids.collect::, _>>().map_err(Into::into), - ) - .await?; - for id in ids { - let src_msg_id: MsgId = id; - let mut msg = Message::load_from_db(context, src_msg_id).await?; - if msg.state == MessageState::OutDraft { - bail!("cannot forward drafts."); - } + let original_param = msg.param.clone(); - let original_param = msg.param.clone(); + // we tested a sort of broadcast + // by not marking own forwarded messages as such, + // however, this turned out to be to confusing and unclear. - // we tested a sort of broadcast - // by not marking own forwarded messages as such, - // however, this turned out to be to confusing and unclear. + if msg.get_viewtype() != Viewtype::Sticker { + msg.param + .set_int(Param::Forwarded, src_msg_id.to_u32() as i32); + } - if msg.get_viewtype() != Viewtype::Sticker { - msg.param - .set_int(Param::Forwarded, src_msg_id.to_u32() as i32); - } + msg.param.remove(Param::GuaranteeE2ee); + msg.param.remove(Param::ForcePlaintext); + msg.param.remove(Param::Cmd); + msg.param.remove(Param::OverrideSenderDisplayname); + msg.param.remove(Param::WebxdcDocument); + msg.param.remove(Param::WebxdcDocumentTimestamp); + msg.param.remove(Param::WebxdcSummary); + msg.param.remove(Param::WebxdcSummaryTimestamp); + msg.in_reply_to = None; - msg.param.remove(Param::GuaranteeE2ee); - msg.param.remove(Param::ForcePlaintext); - msg.param.remove(Param::Cmd); - msg.param.remove(Param::OverrideSenderDisplayname); - msg.param.remove(Param::WebxdcSummary); - msg.param.remove(Param::WebxdcSummaryTimestamp); - msg.in_reply_to = None; + // do not leak data as group names; a default subject is generated by mimefactory + msg.subject = "".to_string(); - // do not leak data as group names; a default subject is generated by mimefactory - msg.subject = "".to_string(); + let new_msg_id: MsgId; + if msg.state == MessageState::OutPreparing { + new_msg_id = chat + .prepare_msg_raw(context, &mut msg, None, curr_timestamp) + .await?; + curr_timestamp += 1; + msg.param = original_param; + msg.id = src_msg_id; - let new_msg_id: MsgId; - if msg.state == MessageState::OutPreparing { - new_msg_id = chat - .prepare_msg_raw(context, &mut msg, None, curr_timestamp) - .await?; - curr_timestamp += 1; - let save_param = msg.param.clone(); - msg.param = original_param; - msg.id = src_msg_id; - - if let Some(old_fwd) = msg.param.get(Param::PrepForwards) { - let new_fwd = format!("{} {}", old_fwd, new_msg_id.to_u32()); - msg.param.set(Param::PrepForwards, new_fwd); - } else { - msg.param - .set(Param::PrepForwards, new_msg_id.to_u32().to_string()); - } - - msg.update_param(context).await?; - msg.param = save_param; + if let Some(old_fwd) = msg.param.get(Param::PrepForwards) { + let new_fwd = format!("{} {}", old_fwd, new_msg_id.to_u32()); + msg.param.set(Param::PrepForwards, new_fwd); } else { - msg.state = MessageState::OutPending; - new_msg_id = chat - .prepare_msg_raw(context, &mut msg, None, curr_timestamp) - .await?; - curr_timestamp += 1; - if create_send_msg_job(context, new_msg_id).await?.is_some() { - context - .scheduler - .interrupt_smtp(InterruptInfo::new(false)) - .await; - } + msg.param + .set(Param::PrepForwards, new_msg_id.to_u32().to_string()); + } + + msg.update_param(context).await?; + } else { + msg.state = MessageState::OutPending; + new_msg_id = chat + .prepare_msg_raw(context, &mut msg, None, curr_timestamp) + .await?; + curr_timestamp += 1; + if create_send_msg_job(context, &mut msg).await?.is_some() { + context + .scheduler + .interrupt_smtp(InterruptInfo::new(false)) + .await; } - created_chats.push(chat_id); - created_msgs.push(new_msg_id); } + created_chats.push(chat_id); + created_msgs.push(new_msg_id); } for (chat_id, msg_id) in created_chats.iter().zip(created_msgs.iter()) { context.emit_msgs_changed(*chat_id, *msg_id); @@ -3485,29 +3658,31 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> { msgs.push(msg) } - if let Some(chat_id) = chat_id { - let chat = Chat::load_from_db(context, chat_id).await?; - for mut msg in msgs { - if msg.get_showpadlock() && !chat.is_protected() { - msg.param.remove(Param::GuaranteeE2ee); - msg.update_param(context).await?; - } - match msg.get_state() { - MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => { - message::update_msg_state(context, msg.id, MessageState::OutPending).await? - } - _ => bail!("unexpected message state"), - } - context.emit_event(EventType::MsgsChanged { - chat_id: msg.chat_id, - msg_id: msg.id, - }); - if create_send_msg_job(context, msg.id).await?.is_some() { - context - .scheduler - .interrupt_smtp(InterruptInfo::new(false)) - .await; + let Some(chat_id) = chat_id else { + return Ok(()); + }; + + let chat = Chat::load_from_db(context, chat_id).await?; + for mut msg in msgs { + if msg.get_showpadlock() && !chat.is_protected() { + msg.param.remove(Param::GuaranteeE2ee); + msg.update_param(context).await?; + } + match msg.get_state() { + MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => { + message::update_msg_state(context, msg.id, MessageState::OutPending).await? } + _ => bail!("unexpected message state"), + } + context.emit_event(EventType::MsgsChanged { + chat_id: msg.chat_id, + msg_id: msg.id, + }); + if create_send_msg_job(context, &mut msg).await?.is_some() { + context + .scheduler + .interrupt_smtp(InterruptInfo::new(false)) + .await; } } Ok(()) @@ -3577,7 +3752,6 @@ pub async fn add_device_msg_with_importance( chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?; let rfc724_mid = create_outgoing_rfc724_mid(None, "@device"); - msg.try_calc_and_set_dimensions(context).await.ok(); prepare_msg_blob(context, msg).await?; let timestamp_sent = create_smeared_timestamp(context); @@ -3623,7 +3797,7 @@ pub async fn add_device_msg_with_importance( timestamp_sent, // timestamp_sent equals timestamp_rcvd msg.viewtype, state, - msg.text.as_ref().cloned().unwrap_or_default(), + &msg.text, msg.param.to_string(), rfc724_mid, ), @@ -3797,7 +3971,7 @@ mod tests { use crate::contact::{Contact, ContactAddress}; use crate::message::delete_msgs; use crate::receive_imf::receive_imf; - use crate::test_utils::TestContext; + use crate::test_utils::{TestContext, TestContextManager}; use tokio::fs; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -3860,7 +4034,7 @@ mod tests { let t = TestContext::new().await; let chat_id = &t.get_self_chat().await.id; let mut msg = Message::new(Viewtype::Text); - msg.set_text(Some("hello".to_string())); + msg.set_text("hello".to_string()); chat_id.set_draft(&t, Some(&mut msg)).await.unwrap(); let draft = chat_id.get_draft(&t).await.unwrap().unwrap(); @@ -3875,12 +4049,12 @@ mod tests { let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?; let mut msg = Message::new(Viewtype::Text); - msg.set_text(Some("hi!".to_string())); + msg.set_text("hi!".to_string()); chat_id.set_draft(&t, Some(&mut msg)).await?; assert!(chat_id.get_draft(&t).await?.is_some()); let mut msg = Message::new(Viewtype::Text); - msg.set_text(Some("another".to_string())); + msg.set_text("another".to_string()); chat_id.set_draft(&t, Some(&mut msg)).await?; assert!(chat_id.get_draft(&t).await?.is_some()); @@ -3895,7 +4069,7 @@ mod tests { let t = TestContext::new_alice().await; let chat_id = &t.get_self_chat().await.id; let mut msg = Message::new(Viewtype::Text); - msg.set_text(Some("hello".to_string())); + msg.set_text("hello".to_string()); chat_id.set_draft(&t, Some(&mut msg)).await?; assert_eq!(msg.id, chat_id.get_draft(&t).await?.unwrap().id); @@ -3909,7 +4083,7 @@ mod tests { let t = TestContext::new_alice().await; let chat_id = &t.get_self_chat().await.id; let mut msg = Message::new(Viewtype::Text); - msg.set_text(Some("hello".to_string())); + msg.set_text("hello".to_string()); assert_eq!(msg.id, MsgId::new_unset()); assert!(chat_id.get_draft_msg_id(&t).await?.is_none()); @@ -3922,7 +4096,7 @@ mod tests { ); assert_eq!(id_after_1st_set, chat_id.get_draft(&t).await?.unwrap().id); - msg.set_text(Some("hello2".to_string())); + msg.set_text("hello2".to_string()); chat_id.set_draft(&t, Some(&mut msg)).await?; let id_after_2nd_set = msg.id; @@ -3934,7 +4108,7 @@ mod tests { let test = chat_id.get_draft(&t).await?.unwrap(); assert_eq!(id_after_2nd_set, test.id); assert_eq!(id_after_2nd_set, msg.id); - assert_eq!(test.text, Some("hello2".to_string())); + assert_eq!(test.text, "hello2".to_string()); assert_eq!(test.state, MessageState::OutDraft); let id_after_prepare = prepare_msg(&t, *chat_id, &mut msg).await?; @@ -3962,11 +4136,11 @@ mod tests { // save a draft let mut draft = Message::new(Viewtype::Text); - draft.set_text(Some("draft text".to_string())); + draft.set_text("draft text".to_string()); chat_id.set_draft(&t, Some(&mut draft)).await?; let test = Message::load_from_db(&t, draft.id).await?; - assert_eq!(test.text, Some("draft text".to_string())); + assert_eq!(test.text, "draft text".to_string()); assert!(test.quoted_text().is_none()); assert!(test.quoted_message(&t).await?.is_none()); @@ -3975,17 +4149,17 @@ mod tests { chat_id.set_draft(&t, Some(&mut draft)).await?; let test = Message::load_from_db(&t, draft.id).await?; - assert_eq!(test.text, Some("draft text".to_string())); + assert_eq!(test.text, "draft text".to_string()); assert_eq!(test.quoted_text(), Some("quote1".to_string())); assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote1.id); // change quote on same message object - draft.set_text(Some("another draft text".to_string())); + draft.set_text("another draft text".to_string()); draft.set_quote(&t, Some("e2)).await?; chat_id.set_draft(&t, Some(&mut draft)).await?; let test = Message::load_from_db(&t, draft.id).await?; - assert_eq!(test.text, Some("another draft text".to_string())); + assert_eq!(test.text, "another draft text".to_string()); assert_eq!(test.quoted_text(), Some("quote2".to_string())); assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote2.id); @@ -3994,7 +4168,7 @@ mod tests { chat_id.set_draft(&t, Some(&mut draft)).await?; let test = Message::load_from_db(&t, draft.id).await?; - assert_eq!(test.text, Some("another draft text".to_string())); + assert_eq!(test.text, "another draft text".to_string()); assert!(test.quoted_text().is_none()); assert!(test.quoted_message(&t).await?.is_none()); @@ -4014,6 +4188,144 @@ mod tests { assert_eq!(added, false); } + /// Test adding and removing members in a group chat. + /// + /// Make sure messages sent outside contain authname + /// and displayed messages contain locally set name. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_member_add_remove() -> Result<()> { + let mut tcm = TestContextManager::new(); + + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + // Disable encryption so we can inspect raw message contents. + alice.set_config(Config::E2eeEnabled, Some("0")).await?; + bob.set_config(Config::E2eeEnabled, Some("0")).await?; + + // Create contact for Bob on the Alice side with name "robert". + let alice_bob_contact_id = Contact::create(&alice, "robert", "bob@example.net").await?; + + // Set Bob authname to "Bob" and send it to Alice. + bob.set_config(Config::Displayname, Some("Bob")).await?; + tcm.send_recv(&bob, &alice, "Hello!").await; + + // Check that Alice has Bob's name set to "robert" and authname set to "Bob". + { + let alice_bob_contact = Contact::get_by_id(&alice, alice_bob_contact_id).await?; + assert_eq!(alice_bob_contact.get_name(), "robert"); + + // This is the name that will be sent outside. + assert_eq!(alice_bob_contact.get_authname(), "Bob"); + + assert_eq!(alice_bob_contact.get_display_name(), "robert"); + } + + // Create and promote a group. + let alice_chat_id = + create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; + let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?; + add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?; + let sent = alice + .send_text(alice_chat_id, "Hi! I created a group.") + .await; + assert!(sent.payload.contains("Hi! I created a group.")); + + // Alice adds Bob to the chat. + add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; + let sent = alice.pop_sent_msg().await; + assert!(sent + .payload + .contains("I added member Bob (bob@example.net).")); + // Locally set name "robert" should not leak. + assert!(!sent.payload.contains("robert")); + assert_eq!( + sent.load_from_db().await.get_text(), + "You added member robert (bob@example.net)." + ); + + // Alice removes Bob from the chat. + remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; + let sent = alice.pop_sent_msg().await; + assert!(sent + .payload + .contains("I removed member Bob (bob@example.net).")); + assert!(!sent.payload.contains("robert")); + assert_eq!( + sent.load_from_db().await.get_text(), + "You removed member robert (bob@example.net)." + ); + + // Alice leaves the chat. + remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?; + let sent = alice.pop_sent_msg().await; + assert!(sent.payload.contains("I left the group.")); + assert_eq!(sent.load_from_db().await.get_text(), "You left the group."); + + Ok(()) + } + + /// Test simultaneous removal of user from the chat and leaving the group. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_simultaneous_member_remove() -> Result<()> { + let mut tcm = TestContextManager::new(); + + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + alice.set_config(Config::E2eeEnabled, Some("0")).await?; + bob.set_config(Config::E2eeEnabled, Some("0")).await?; + + let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?; + let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?; + let alice_claire_contact_id = + Contact::create(&alice, "Claire", "claire@example.net").await?; + + // Create and promote a group. + let alice_chat_id = + create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; + add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; + add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?; + let alice_sent_msg = alice + .send_text(alice_chat_id, "Hi! I created a group.") + .await; + let bob_received_msg = bob.recv_msg(&alice_sent_msg).await; + + let bob_chat_id = bob_received_msg.get_chat_id(); + bob_chat_id.accept(&bob).await?; + + // Alice adds Claire to the chat. + add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?; + let alice_sent_add_msg = alice.pop_sent_msg().await; + + // Alice removes Bob from the chat. + remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; + let alice_sent_remove_msg = alice.pop_sent_msg().await; + + // Bob leaves the chat. + remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?; + + // Bob receives a msg about Alice adding Claire to the group. + let bob_received_add_msg = bob.recv_msg(&alice_sent_add_msg).await; + + // Test that add message is rewritten. + assert_eq!( + bob_received_add_msg.get_text(), + "Member claire@example.net added by alice@example.org." + ); + + // Bob receives a msg about Alice removing him from the group. + let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await; + + // Test that remove message is rewritten. + assert_eq!( + bob_received_remove_msg.get_text(), + "Member Me (bob@example.net) removed by alice@example.org." + ); + + Ok(()) + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_modify_chat_multi_device() -> Result<()> { let a1 = TestContext::new_alice().await; @@ -4210,6 +4522,7 @@ mod tests { Ok(()) } + /// Test that adding or removing contacts in 1:1 chat is not allowed. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_add_remove_contact_for_single() { let ctx = TestContext::new_alice().await; @@ -4257,7 +4570,7 @@ mod tests { t2.recv_msg(&sent_msg).await; let chat = &t2.get_self_chat().await; let msg = t2.get_last_msg_in(chat.id).await; - assert_eq!(msg.text, Some("foo self".to_string())); + assert_eq!(msg.text, "foo self".to_string()); assert_eq!(msg.from_id, ContactId::SELF); assert_eq!(msg.to_id, ContactId::SELF); assert!(msg.get_showpadlock()); @@ -4271,12 +4584,12 @@ mod tests { // add two device-messages let mut msg1 = Message::new(Viewtype::Text); - msg1.text = Some("first message".to_string()); + msg1.set_text("first message".to_string()); let msg1_id = add_device_msg(&t, None, Some(&mut msg1)).await; assert!(msg1_id.is_ok()); let mut msg2 = Message::new(Viewtype::Text); - msg2.text = Some("second message".to_string()); + msg2.set_text("second message".to_string()); let msg2_id = add_device_msg(&t, None, Some(&mut msg2)).await; assert!(msg2_id.is_ok()); assert_ne!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap()); @@ -4285,7 +4598,7 @@ mod tests { let msg1 = message::Message::load_from_db(&t, msg1_id.unwrap()).await; assert!(msg1.is_ok()); let msg1 = msg1.unwrap(); - assert_eq!(msg1.text.as_ref().unwrap(), "first message"); + assert_eq!(msg1.text, "first message"); assert_eq!(msg1.from_id, ContactId::DEVICE); assert_eq!(msg1.to_id, ContactId::SELF); assert!(!msg1.is_info()); @@ -4294,7 +4607,7 @@ mod tests { let msg2 = message::Message::load_from_db(&t, msg2_id.unwrap()).await; assert!(msg2.is_ok()); let msg2 = msg2.unwrap(); - assert_eq!(msg2.text.as_ref().unwrap(), "second message"); + assert_eq!(msg2.text, "second message"); // check device chat assert_eq!(msg2.chat_id.get_msg_cnt(&t).await.unwrap(), 2); @@ -4306,13 +4619,13 @@ mod tests { // add two device-messages with the same label (second attempt is not added) let mut msg1 = Message::new(Viewtype::Text); - msg1.text = Some("first message".to_string()); + msg1.text = "first message".to_string(); let msg1_id = add_device_msg(&t, Some("any-label"), Some(&mut msg1)).await; assert!(msg1_id.is_ok()); assert!(!msg1_id.as_ref().unwrap().is_unset()); let mut msg2 = Message::new(Viewtype::Text); - msg2.text = Some("second message".to_string()); + msg2.text = "second message".to_string(); let msg2_id = add_device_msg(&t, Some("any-label"), Some(&mut msg2)).await; assert!(msg2_id.is_ok()); assert!(msg2_id.as_ref().unwrap().is_unset()); @@ -4320,7 +4633,7 @@ mod tests { // check added message let msg1 = message::Message::load_from_db(&t, *msg1_id.as_ref().unwrap()).await?; assert_eq!(msg1_id.as_ref().unwrap(), &msg1.id); - assert_eq!(msg1.text.as_ref().unwrap(), "first message"); + assert_eq!(msg1.text, "first message"); assert_eq!(msg1.from_id, ContactId::DEVICE); assert_eq!(msg1.to_id, ContactId::SELF); assert!(!msg1.is_info()); @@ -4360,7 +4673,7 @@ mod tests { assert!(res.is_ok()); let mut msg = Message::new(Viewtype::Text); - msg.text = Some("message text".to_string()); + msg.set_text("message text".to_string()); let msg_id = add_device_msg(&t, Some("some-label"), Some(&mut msg)).await; assert!(msg_id.is_ok()); @@ -4378,7 +4691,7 @@ mod tests { assert!(was_device_msg_ever_added(&t, "some-label").await.unwrap()); let mut msg = Message::new(Viewtype::Text); - msg.text = Some("message text".to_string()); + msg.set_text("message text".to_string()); add_device_msg(&t, Some("another-label"), Some(&mut msg)) .await .ok(); @@ -4396,7 +4709,7 @@ mod tests { let t = TestContext::new().await; let mut msg = Message::new(Viewtype::Text); - msg.text = Some("message text".to_string()); + msg.set_text("message text".to_string()); add_device_msg(&t, Some("some-label"), Some(&mut msg)) .await .ok(); @@ -4420,7 +4733,7 @@ mod tests { .unwrap(); let mut msg = Message::new(Viewtype::Text); - msg.text = Some("message text".to_string()); + msg.set_text("message text".to_string()); assert!(send_msg(&t, device_chat_id, &mut msg).await.is_err()); assert!(prepare_msg(&t, device_chat_id, &mut msg).await.is_err()); @@ -4432,7 +4745,7 @@ mod tests { async fn test_delete_and_reset_all_device_msgs() { let t = TestContext::new().await; let mut msg = Message::new(Viewtype::Text); - msg.text = Some("message text".to_string()); + msg.set_text("message text".to_string()); let msg_id1 = add_device_msg(&t, Some("some-label"), Some(&mut msg)) .await .unwrap(); @@ -4465,7 +4778,7 @@ mod tests { // create two chats let t = TestContext::new().await; let mut msg = Message::new(Viewtype::Text); - msg.text = Some("foo".to_string()); + msg.set_text("foo".to_string()); let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap(); let chat_id1 = message::Message::load_from_db(&t, msg_id) .await @@ -4746,7 +5059,7 @@ mod tests { // create 3 chats, wait 1 second in between to get a reliable order (we order by time) let mut msg = Message::new(Viewtype::Text); - msg.text = Some("foo".to_string()); + msg.set_text("foo".to_string()); let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap(); let chat_id1 = message::Message::load_from_db(&t, msg_id) .await @@ -4824,7 +5137,7 @@ mod tests { ); let mut msg = Message::new(Viewtype::Text); - msg.set_text(Some("hi!".into())); + msg.set_text("hi!".into()); let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await; let msg = alice.recv_msg(&sent_msg).await; assert_eq!(msg.chat_id, alice_chat_id); @@ -4962,7 +5275,7 @@ mod tests { let msg = t.get_last_msg_in(chat_id).await; assert_eq!(msg.get_chat_id(), chat_id); assert_eq!(msg.get_viewtype(), Viewtype::Text); - assert_eq!(msg.get_text().unwrap(), "foo info"); + assert_eq!(msg.get_text(), "foo info"); assert!(msg.is_info()); assert_eq!(msg.get_info_type(), SystemMessage::Unknown); assert!(msg.parent(&t).await?.is_none()); @@ -4989,7 +5302,7 @@ mod tests { let msg = Message::load_from_db(&t, msg_id).await?; assert_eq!(msg.get_chat_id(), chat_id); assert_eq!(msg.get_viewtype(), Viewtype::Text); - assert_eq!(msg.get_text().unwrap(), "foo bar info"); + assert_eq!(msg.get_text(), "foo bar info"); assert!(msg.is_info()); assert_eq!(msg.get_info_type(), SystemMessage::EphemeralTimerChanged); assert!(msg.parent(&t).await?.is_none()); @@ -5000,72 +5313,6 @@ mod tests { Ok(()) } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_protection() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config_bool(Config::BccSelf, false).await?; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let chat = Chat::load_from_db(&t, chat_id).await?; - assert!(!chat.is_protected()); - assert!(chat.is_unpromoted()); - - // enable protection on unpromoted chat, the info-message is added via add_info_msg() - chat_id - .set_protection(&t, ProtectionStatus::Protected) - .await?; - - let chat = Chat::load_from_db(&t, chat_id).await?; - assert!(chat.is_protected()); - assert!(chat.is_unpromoted()); - - let msgs = get_chat_msgs(&t, chat_id).await?; - assert_eq!(msgs.len(), 1); - - let msg = t.get_last_msg_in(chat_id).await; - assert!(msg.is_info()); - assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled); - assert_eq!(msg.get_state(), MessageState::InNoticed); - - // disable protection again, still unpromoted - chat_id - .set_protection(&t, ProtectionStatus::Unprotected) - .await?; - - let chat = Chat::load_from_db(&t, chat_id).await?; - assert!(!chat.is_protected()); - assert!(chat.is_unpromoted()); - - let msg = t.get_last_msg_in(chat_id).await; - assert!(msg.is_info()); - assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionDisabled); - assert_eq!(msg.get_state(), MessageState::InNoticed); - - // send a message, this switches to promoted state - send_text_msg(&t, chat_id, "hi!".to_string()).await?; - - let chat = Chat::load_from_db(&t, chat_id).await?; - assert!(!chat.is_protected()); - assert!(!chat.is_unpromoted()); - - let msgs = get_chat_msgs(&t, chat_id).await?; - assert_eq!(msgs.len(), 3); - - // enable protection on promoted chat, the info-message is sent via send_msg() this time - chat_id - .set_protection(&t, ProtectionStatus::Protected) - .await?; - let chat = Chat::load_from_db(&t, chat_id).await?; - assert!(chat.is_protected()); - assert!(!chat.is_unpromoted()); - - let msg = t.get_last_msg_in(chat_id).await; - assert!(msg.is_info()); - assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled); - assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat - - Ok(()) - } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_lookup_by_contact_id() { let ctx = TestContext::new_alice().await; @@ -5147,9 +5394,11 @@ mod tests { // Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com). let sent_msg = alice.pop_sent_msg().await; let msg = sent_msg.payload(); - assert_eq!(msg.match_indices("Gr.").count(), 2); + assert_eq!(msg.match_indices("Message-ID: Result<()> { + async fn test_sticker( + filename: &str, + bytes: &[u8], + res_viewtype: Viewtype, + w: i32, + h: i32, + ) -> Result<()> { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; let alice_chat = alice.create_chat(&bob).await; @@ -5383,12 +5638,19 @@ mod tests { let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; let mime = sent_msg.payload(); - assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1); + if res_viewtype == Viewtype::Sticker { + assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1); + } let msg = bob.recv_msg(&sent_msg).await; assert_eq!(msg.chat_id, bob_chat.id); - assert_eq!(msg.get_viewtype(), Viewtype::Sticker); - assert_eq!(msg.get_filename(), Some(filename.to_string())); + assert_eq!(msg.get_viewtype(), res_viewtype); + let msg_filename = msg.get_filename().unwrap(); + match res_viewtype { + Viewtype::Sticker => assert_eq!(msg_filename, filename), + Viewtype::Image => assert!(msg_filename.starts_with("image_")), + _ => panic!("Not implemented"), + } assert_eq!(msg.get_width(), w); assert_eq!(msg.get_height(), h); assert!(msg.get_filebytes(&bob).await?.unwrap() > 250); @@ -5400,9 +5662,10 @@ mod tests { async fn test_sticker_png() -> Result<()> { test_sticker( "sticker.png", - include_bytes!("../test-data/image/avatar64x64.png"), - 64, - 64, + include_bytes!("../test-data/image/logo.png"), + Viewtype::Sticker, + 135, + 135, ) .await } @@ -5412,19 +5675,66 @@ mod tests { test_sticker( "sticker.jpg", include_bytes!("../test-data/image/avatar1000x1000.jpg"), + Viewtype::Image, 1000, 1000, ) .await } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_sticker_jpeg_force() { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + + let file = alice.get_blobdir().join("sticker.jpg"); + tokio::fs::write( + &file, + include_bytes!("../test-data/image/avatar1000x1000.jpg"), + ) + .await + .unwrap(); + + // Images without force_sticker should be turned into [Viewtype::Image] + let mut msg = Message::new(Viewtype::Sticker); + msg.set_file(file.to_str().unwrap(), None); + let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; + let msg = bob.recv_msg(&sent_msg).await; + assert_eq!(msg.get_viewtype(), Viewtype::Image); + + // Images with `force_sticker = true` should keep [Viewtype::Sticker] + let mut msg = Message::new(Viewtype::Sticker); + msg.set_file(file.to_str().unwrap(), None); + msg.force_sticker(); + let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; + let msg = bob.recv_msg(&sent_msg).await; + assert_eq!(msg.get_viewtype(), Viewtype::Sticker); + + // Images with `force_sticker = true` should keep [Viewtype::Sticker] + // even on drafted messages + let mut msg = Message::new(Viewtype::Sticker); + msg.set_file(file.to_str().unwrap(), None); + msg.force_sticker(); + alice_chat + .id + .set_draft(&alice, Some(&mut msg)) + .await + .unwrap(); + let mut msg = alice_chat.id.get_draft(&alice).await.unwrap().unwrap(); + let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; + let msg = bob.recv_msg(&sent_msg).await; + assert_eq!(msg.get_viewtype(), Viewtype::Sticker); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sticker_gif() -> Result<()> { test_sticker( "sticker.gif", - include_bytes!("../test-data/image/image100x50.gif"), - 100, - 50, + include_bytes!("../test-data/image/logo.gif"), + Viewtype::Sticker, + 135, + 135, ) .await } @@ -5438,8 +5748,8 @@ mod tests { let bob_chat = bob.create_chat(&alice).await; // create sticker - let file_name = "sticker.jpg"; - let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg"); + let file_name = "sticker.png"; + let bytes = include_bytes!("../test-data/image/logo.png"); let file = alice.get_blobdir().join(file_name); tokio::fs::write(&file, bytes).await?; let mut msg = Message::new(Viewtype::Sticker); @@ -5468,7 +5778,7 @@ mod tests { let bob_chat = bob.create_chat(&alice).await; let mut msg = Message::new(Viewtype::Text); - msg.set_text(Some("Hi Bob".to_owned())); + msg.set_text("Hi Bob".to_owned()); let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await; let msg = bob.recv_msg(&sent_msg).await; @@ -5476,7 +5786,7 @@ mod tests { let forwarded_msg = bob.pop_sent_msg().await; let msg = alice.recv_msg(&forwarded_msg).await; - assert!(msg.get_text().unwrap() == "Hi Bob"); + assert_eq!(msg.get_text(), "Hi Bob"); assert!(msg.is_forwarded()); Ok(()) } @@ -5491,7 +5801,7 @@ mod tests { add_contact_to_chat(&t, chat_id1, bob_id).await?; let msg1 = t.get_last_msg_in(chat_id1).await; assert!(msg1.is_info()); - assert!(msg1.get_text().unwrap().contains("bob@example.net")); + assert!(msg1.get_text().contains("bob@example.net")); let chat_id2 = ChatId::create_for_contact(&t, bob_id).await?; assert_eq!(get_chat_msgs(&t, chat_id2).await?.len(), 0); @@ -5501,7 +5811,7 @@ mod tests { assert_eq!(msg2.get_info_type(), SystemMessage::Unknown); assert_ne!(msg2.from_id, ContactId::INFO); assert_ne!(msg2.to_id, ContactId::INFO); - assert_eq!(msg2.get_text().unwrap(), msg1.get_text().unwrap()); + assert_eq!(msg2.get_text(), msg1.get_text()); assert!(msg2.is_forwarded()); Ok(()) @@ -5520,7 +5830,7 @@ mod tests { // Bob quotes received message and sends a reply to Alice. let mut reply = Message::new(Viewtype::Text); - reply.set_text(Some("Reply".to_owned())); + reply.set_text("Reply".to_owned()); reply.set_quote(&bob, Some(&received_msg)).await?; let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await; let received_reply = alice.recv_msg(&sent_reply).await; @@ -5571,13 +5881,13 @@ mod tests { // Alice sends a message to Bob. let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await; let received_msg = bob.recv_msg(&sent_msg).await; - assert_eq!(received_msg.get_text(), Some("Hi Bob".to_string())); + assert_eq!(received_msg.get_text(), "Hi Bob"); assert_eq!(received_msg.chat_id, bob_chat.id); // Alice sends another message to Bob, this has first message as a parent. let sent_msg = alice.send_text(alice_chat.id, "Hello Bob").await; let received_msg = bob.recv_msg(&sent_msg).await; - assert_eq!(received_msg.get_text(), Some("Hello Bob".to_string())); + assert_eq!(received_msg.get_text(), "Hello Bob"); assert_eq!(received_msg.chat_id, bob_chat.id); // Bob forwards message to a group chat with Alice. @@ -5604,7 +5914,7 @@ mod tests { create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?; add_contact_to_chat(&alice, group_id, bob_id).await?; let mut msg = Message::new(Viewtype::Text); - msg.set_text(Some("bla foo".to_owned())); + msg.set_text("bla foo".to_owned()); let sent_msg = alice.send_msg(group_id, &mut msg).await; assert!(sent_msg.payload().contains("secretgrpname")); assert!(sent_msg.payload().contains("secretname")); @@ -5661,7 +5971,7 @@ mod tests { // Bob receives all messages let bob = TestContext::new_bob().await; let msg = bob.recv_msg(&sent1).await; - assert_eq!(msg.get_text().unwrap(), "alice->bob"); + assert_eq!(msg.get_text(), "alice->bob"); assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2); assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1); bob.recv_msg(&sent2).await; @@ -5678,7 +5988,7 @@ mod tests { claire.configure_addr("claire@example.org").await; claire.recv_msg(&sent2).await; let msg = claire.recv_msg(&sent3).await; - assert_eq!(msg.get_text().unwrap(), "alice->bob"); + assert_eq!(msg.get_text(), "alice->bob"); assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3); assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2); let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?; @@ -5816,22 +6126,40 @@ mod tests { get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(), ) .await?; - let chat = Chat::load_from_db(&alice, broadcast_id).await?; - assert_eq!(chat.typ, Chattype::Broadcast); - assert_eq!(chat.name, stock_str::broadcast_list(&alice).await); - assert!(!chat.is_self_talk()); + set_chat_name(&alice, broadcast_id, "Broadcast list").await?; + { + let chat = Chat::load_from_db(&alice, broadcast_id).await?; + assert_eq!(chat.typ, Chattype::Broadcast); + assert_eq!(chat.name, "Broadcast list"); + assert!(!chat.is_self_talk()); - send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?; - let msg = alice.get_last_msg().await; - assert_eq!(msg.chat_id, chat.id); + send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?; + let msg = alice.get_last_msg().await; + assert_eq!(msg.chat_id, chat.id); + } - let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; - assert_eq!(msg.get_text(), Some("ola!".to_string())); - assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data - let chat = Chat::load_from_db(&bob, msg.chat_id).await?; - assert_eq!(chat.typ, Chattype::Single); - assert_eq!(chat.id, chat_bob.id); - assert!(!chat.is_self_talk()); + { + let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; + assert_eq!(msg.get_text(), "ola!"); + assert_eq!(msg.subject, "Broadcast list"); + assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data + let chat = Chat::load_from_db(&bob, msg.chat_id).await?; + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_ne!(chat.id, chat_bob.id); + assert_eq!(chat.name, "Broadcast list"); + assert!(!chat.is_self_talk()); + } + + { + // Alice changes the name: + set_chat_name(&alice, broadcast_id, "My great broadcast").await?; + let sent = alice.send_text(broadcast_id, "I changed the title!").await; + + let msg = bob.recv_msg(&sent).await; + assert_eq!(msg.subject, "Re: My great broadcast"); + let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await?; + assert_eq!(bob_chat.name, "My great broadcast"); + } Ok(()) } @@ -5976,7 +6304,7 @@ mod tests { chat_id1, Viewtype::Sticker, "b.png", - include_bytes!("../test-data/image/avatar64x64.png"), + include_bytes!("../test-data/image/logo.png"), ) .await?; let second_image_msg_id = send_media( diff --git a/src/chatlist.rs b/src/chatlist.rs index ddd60c072..883ca780c 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -1,6 +1,7 @@ //! # Chat list module. use anyhow::{ensure, Context as _, Result}; +use once_cell::sync::Lazy; use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility}; use crate::constants::{ @@ -10,8 +11,14 @@ use crate::constants::{ use crate::contact::{Contact, ContactId}; use crate::context::Context; use crate::message::{Message, MessageState, MsgId}; +use crate::param::{Param, Params}; use crate::stock_str; use crate::summary::Summary; +use crate::tools::IsNoneOrEmpty; + +/// Regex to find out if a query should filter by unread messages. +pub static IS_UNREAD_FILTER: Lazy = + Lazy::new(|| regex::Regex::new(r"\bis:unread\b").unwrap()); /// An object representing a single chatlist in memory. /// @@ -76,7 +83,8 @@ impl Chatlist { /// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT /// is added as needed. /// `query`: An optional query for filtering the list. Only chats matching this query - /// are returned. + /// are returned. When `is:unread` is contained in the query, the chatlist is + /// filtered such that only chats with unread messages show up. /// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID /// are returned. pub async fn try_load( @@ -170,8 +178,10 @@ impl Chatlist { ) .await? } else if let Some(query) = query { - let query = query.trim().to_string(); - ensure!(!query.is_empty(), "missing query"); + let mut query = query.trim().to_string(); + ensure!(!query.is_empty(), "query mustn't be empty"); + let only_unread = IS_UNREAD_FILTER.find(&query).is_some(); + query = IS_UNREAD_FILTER.replace(&query, "").trim().to_string(); // allow searching over special names that may change at any time // when the ui calls set_stock_translation() @@ -196,42 +206,93 @@ impl Chatlist { WHERE c.id>9 AND c.id!=?2 AND c.blocked!=1 AND c.name LIKE ?3 + AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0)) GROUP BY c.id ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - (MessageState::OutDraft, skip_id, str_like_cmd), + (MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh), process_row, process_rows, ) .await? } else { - // show normal chatlist - let sort_id_up = if flag_for_forwarding { - ChatId::lookup_by_contact(context, ContactId::SELF) + let mut ids = if flag_for_forwarding { + let sort_id_up = ChatId::lookup_by_contact(context, ContactId::SELF) .await? - .unwrap_or_default() + .unwrap_or_default(); + let process_row = |row: &rusqlite::Row| { + let chat_id: ChatId = row.get(0)?; + let typ: Chattype = row.get(1)?; + let param: Params = row.get::<_, String>(2)?.parse().unwrap_or_default(); + let msg_id: Option = row.get(3)?; + Ok((chat_id, typ, param, msg_id)) + }; + let process_rows = |rows: rusqlite::MappedRows<_>| { + rows.filter_map(|row: std::result::Result<(_, _, Params, _), _>| match row { + Ok((chat_id, typ, param, msg_id)) => { + if typ == Chattype::Mailinglist + && param.get(Param::ListPost).is_none_or_empty() + { + None + } else { + Some(Ok((chat_id, msg_id))) + } + } + Err(e) => Some(Err(e)), + }) + .collect::, _>>() + .map_err(Into::into) + }; + // Return ProtectionBroken chats also, as that may happen to a verified chat at any + // time. It may be confusing if a chat that is normally in the list disappears + // suddenly. The UI need to deal with that case anyway. + context.sql.query_map( + "SELECT c.id, c.type, c.param, m.id + FROM chats c + LEFT JOIN msgs m + ON c.id=m.chat_id + AND m.id=( + SELECT id + FROM msgs + WHERE chat_id=c.id + AND (hidden=0 OR state=?) + ORDER BY timestamp DESC, id DESC LIMIT 1) + WHERE c.id>9 AND c.id!=? + AND c.blocked=0 + AND NOT c.archived=? + AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)) + GROUP BY c.id + ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", + ( + MessageState::OutDraft, skip_id, ChatVisibility::Archived, + Chattype::Group, ContactId::SELF, + sort_id_up, ChatVisibility::Pinned, + ), + process_row, + process_rows, + ).await? } else { - ChatId::new(0) + // show normal chatlist + context.sql.query_map( + "SELECT c.id, m.id + FROM chats c + LEFT JOIN msgs m + ON c.id=m.chat_id + AND m.id=( + SELECT id + FROM msgs + WHERE chat_id=c.id + AND (hidden=0 OR state=?) + ORDER BY timestamp DESC, id DESC LIMIT 1) + WHERE c.id>9 AND c.id!=? + AND (c.blocked=0 OR c.blocked=2) + AND NOT c.archived=? + GROUP BY c.id + ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", + (MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned), + process_row, + process_rows, + ).await? }; - let mut ids = context.sql.query_map( - "SELECT c.id, m.id - FROM chats c - LEFT JOIN msgs m - ON c.id=m.chat_id - AND m.id=( - SELECT id - FROM msgs - WHERE chat_id=c.id - AND (hidden=0 OR state=?1) - ORDER BY timestamp DESC, id DESC LIMIT 1) - WHERE c.id>9 AND c.id!=?2 - AND (c.blocked=0 OR (c.blocked=2 AND NOT ?3)) - AND NOT c.archived=?4 - GROUP BY c.id - ORDER BY c.id=?5 DESC, c.archived=?6 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - (MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned), - process_row, - process_rows, - ).await?; if !flag_no_specials && get_archived_cnt(context).await? > 0 { if ids.is_empty() && flag_add_alldone_hint { ids.push((DC_CHAT_ID_ALLDONE_HINT, None)); @@ -244,6 +305,27 @@ impl Chatlist { Ok(Chatlist { ids }) } + /// Converts list of chat IDs to a chatlist. + pub(crate) async fn from_chat_ids(context: &Context, chat_ids: &[ChatId]) -> Result { + let mut ids = Vec::new(); + for &chat_id in chat_ids { + let msg_id: Option = context + .sql + .query_get_value( + "SELECT id + FROM msgs + WHERE chat_id=?1 + AND (hidden=0 OR state=?2) + ORDER BY timestamp DESC, id DESC LIMIT 1", + (chat_id, MessageState::OutDraft), + ) + .await + .with_context(|| format!("failed to get msg ID for chat {}", chat_id))?; + ids.push((chat_id, msg_id)); + } + Ok(Chatlist { ids }) + } + /// Find out the number of chats. pub fn len(&self) -> usize { self.ids.len() @@ -311,16 +393,20 @@ impl Chatlist { }; let (lastmsg, lastcontact) = if let Some(lastmsg_id) = lastmsg_id { - let lastmsg = Message::load_from_db(context, lastmsg_id).await?; + let lastmsg = Message::load_from_db(context, lastmsg_id) + .await + .context("loading message failed")?; if lastmsg.from_id == ContactId::SELF { (Some(lastmsg), None) } else { match chat.typ { Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => { - let lastcontact = Contact::load_from_db(context, lastmsg.from_id).await?; + let lastcontact = Contact::get_by_id(context, lastmsg.from_id) + .await + .context("loading contact failed")?; (Some(lastmsg), Some(lastcontact)) } - Chattype::Single | Chattype::Undefined => (Some(lastmsg), None), + Chattype::Single => (Some(lastmsg), None), } } } else { @@ -362,10 +448,32 @@ pub async fn get_archived_cnt(context: &Context) -> Result { Ok(count) } +/// Gets the last message of a chat, the message that would also be displayed in the ChatList +/// Used for passing to `deltachat::chatlist::Chatlist::get_summary2` +pub async fn get_last_message_for_chat( + context: &Context, + chat_id: ChatId, +) -> Result> { + context + .sql + .query_get_value( + "SELECT id + FROM msgs + WHERE chat_id=?2 + AND (hidden=0 OR state=?1) + ORDER BY timestamp DESC, id DESC LIMIT 1", + (MessageState::OutDraft, chat_id), + ) + .await +} + #[cfg(test)] mod tests { use super::*; - use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus}; + use crate::chat::{ + add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat, + send_text_msg, ProtectionStatus, + }; use crate::message::Viewtype; use crate::receive_imf::receive_imf; use crate::stock_str::StockMessage; @@ -373,7 +481,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_try_load() { - let t = TestContext::new().await; + let t = TestContext::new_bob().await; let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat") .await .unwrap(); @@ -401,7 +509,7 @@ mod tests { // 2s here. for chat_id in &[chat_id1, chat_id3, chat_id2] { let mut msg = Message::new(Viewtype::Text); - msg.set_text(Some("hello".to_string())); + msg.set_text("hello".to_string()); chat_id.set_draft(&t, Some(&mut msg)).await.unwrap(); } @@ -412,6 +520,31 @@ mod tests { let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap(); assert_eq!(chats.len(), 1); + // receive a message from alice + let alice = TestContext::new_alice().await; + let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "alice chat") + .await + .unwrap(); + add_contact_to_chat( + &alice, + alice_chat_id, + Contact::create(&alice, "bob", "bob@example.net") + .await + .unwrap(), + ) + .await + .unwrap(); + send_text_msg(&alice, alice_chat_id, "hi".into()) + .await + .unwrap(); + let sent_msg = alice.pop_sent_msg().await; + + t.recv_msg(&sent_msg).await; + let chats = Chatlist::try_load(&t, 0, Some("is:unread"), None) + .await + .unwrap(); + assert!(chats.len() == 1); + let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None) .await .unwrap(); @@ -450,6 +583,14 @@ mod tests { .await .unwrap() .is_self_talk()); + + remove_contact_from_chat(&t, chats.get_chat_id(1).unwrap(), ContactId::SELF) + .await + .unwrap(); + let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None) + .await + .unwrap(); + assert!(chats.len() == 1); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -613,7 +754,7 @@ mod tests { .unwrap(); let mut msg = Message::new(Viewtype::Text); - msg.set_text(Some("foo:\nbar \r\n test".to_string())); + msg.set_text("foo:\nbar \r\n test".to_string()); chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap(); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); diff --git a/src/config.rs b/src/config.rs index 3527e0096..a5bf6fa70 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ //! # Key-value configuration management. use std::env; +use std::path::Path; use std::str::FromStr; use anyhow::{ensure, Context as _, Result}; @@ -145,7 +146,7 @@ pub enum Config { /// If set to "1", on the first time `start_io()` is called after configuring, /// the newest existing messages are fetched. /// Existing recipients are added to the contact database regardless of this setting. - #[strum(props(default = "1"))] + #[strum(props(default = "0"))] FetchExistingMsgs, /// If set to "1", then existing messages are considered to be already fetched. @@ -285,6 +286,12 @@ pub enum Config { #[strum(props(default = "60"))] ScanAllFoldersDebounceSecs, + /// Whether to avoid using IMAP IDLE even if the server supports it. + /// + /// This is a developer option for testing "fake idle". + #[strum(props(default = "0"))] + DisableIdle, + /// Defines the max. size (in bytes) of messages downloaded automatically. /// 0 = no limit. #[strum(props(default = "0"))] @@ -311,6 +318,23 @@ pub enum Config { /// Last message processed by the bot. LastMsgId, + + /// How often to gossip Autocrypt keys in chats with multiple recipients, in seconds. 2 days by + /// default. + /// + /// This is not supposed to be changed by UIs and only used for testing. + #[strum(props(default = "172800"))] + GossipPeriod, + + /// Feature flag for verified 1:1 chats; the UI should set it + /// to 1 if it supports verified 1:1 chats. + /// Regardless of this setting, `chat.is_protected()` returns true while the key is verified, + /// and when the key changes, an info message is posted into the chat. + /// 0=Nothing else happens when the key changes. + /// 1=After the key changed, `can_send()` returns false and `is_protection_broken()` returns true + /// until `chat_id.accept()` is called. + #[strum(props(default = "0"))] + VerifiedOneOnOneChats, } impl Context { @@ -329,7 +353,11 @@ impl Context { let value = match key { Config::Selfavatar => { let rel_path = self.sql.get_raw_config(key.as_ref()).await?; - rel_path.map(|p| get_abs_path(self, p).to_string_lossy().into_owned()) + rel_path.map(|p| { + get_abs_path(self, Path::new(&p)) + .to_string_lossy() + .into_owned() + }) } Config::SysVersion => Some((*DC_VERSION_STR).clone()), Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")), @@ -460,6 +488,28 @@ impl Context { .set_raw_config(key.as_ref(), value.as_deref()) .await?; } + Config::Socks5Enabled + | Config::BccSelf + | Config::E2eeEnabled + | Config::MdnsEnabled + | Config::SentboxWatch + | Config::MvboxMove + | Config::OnlyFetchMvbox + | Config::FetchExistingMsgs + | Config::DeleteToTrash + | Config::SaveMimeHeaders + | Config::Configured + | Config::Bot + | Config::NotifyAboutWrongPw + | Config::SendSyncMsgs + | Config::SignUnencrypted + | Config::DisableIdle => { + ensure!( + matches!(value, None | Some("0") | Some("1")), + "Boolean value must be either 0 or 1" + ); + self.sql.set_raw_config(key.as_ref(), value).await?; + } _ => { self.sql.set_raw_config(key.as_ref(), value).await?; } @@ -609,6 +659,18 @@ mod tests { ); } + /// Tests that "bot" config can only be set to "0" or "1". + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_set_config_bot() { + let t = TestContext::new().await; + + assert!(t.set_config(Config::Bot, None).await.is_ok()); + assert!(t.set_config(Config::Bot, Some("0")).await.is_ok()); + assert!(t.set_config(Config::Bot, Some("1")).await.is_ok()); + assert!(t.set_config(Config::Bot, Some("2")).await.is_err()); + assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err()); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_media_quality_config_option() { let t = TestContext::new().await; diff --git a/src/configure.rs b/src/configure.rs index bffdedc44..823ca302c 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -1,8 +1,16 @@ -//! Email accounts autoconfiguration process module. +//! # Email accounts autoconfiguration process. +//! +//! The module provides automatic lookup of configuration +//! for email providers based on the built-in [provider database], +//! [Mozilla Thunderbird Autoconfiguration protocol] +//! and [Outlook's Autodiscover]. +//! +//! [provider database]: crate::provider +//! [Mozilla Thunderbird Autoconfiguration protocol]: auto_mozilla +//! [Outlook's Autodiscover]: auto_outlook mod auto_mozilla; mod auto_outlook; -mod read_url; mod server_params; use anyhow::{bail, ensure, Context as _, Result}; @@ -18,7 +26,6 @@ use crate::config::Config; use crate::contact::addr_cmp; use crate::context::Context; use crate::imap::Imap; -use crate::job; use crate::log::LogExt; use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam}; use crate::message::{Message, Viewtype}; @@ -120,8 +127,8 @@ async fn on_configure_completed( old_addr: Option, ) -> Result<()> { if let Some(provider) = param.provider { - if let Some(config_defaults) = &provider.config_defaults { - for def in config_defaults.iter() { + if let Some(config_defaults) = provider.config_defaults { + for def in config_defaults { if !context.config_exists(def.key).await? { info!(context, "apply config_defaults {}={}", def.key, def.value); context.set_config(def.key, Some(def.value)).await?; @@ -136,7 +143,7 @@ async fn on_configure_completed( if !provider.after_login_hint.is_empty() { let mut msg = Message::new(Viewtype::Text); - msg.text = Some(provider.after_login_hint.to_string()); + msg.text = provider.after_login_hint.to_string(); if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg)) .await .is_err() @@ -151,7 +158,7 @@ async fn on_configure_completed( if !addr_cmp(&new_addr, &old_addr) { let mut msg = Message::new(Viewtype::Text); msg.text = - Some(stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await); + stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await; chat::add_device_msg(context, None, Some(&mut msg)) .await .context("Cannot add AEAP explanation") @@ -308,7 +315,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { } // respect certificate setting from function parameters - for mut server in &mut servers { + for server in &mut servers { let certificate_checks = match server.protocol { Protocol::Imap => param.imap.certificate_checks, Protocol::Smtp => param.smtp.certificate_checks, @@ -452,9 +459,12 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { progress!(ctx, 910); - if ctx.get_config(Config::ConfiguredAddr).await?.as_deref() != Some(¶m.addr) { - // Switched account, all server UIDs we know are invalid - job::schedule_resync(ctx).await?; + if let Some(configured_addr) = ctx.get_config(Config::ConfiguredAddr).await? { + if configured_addr != param.addr { + // Switched account, all server UIDs we know are invalid + info!(ctx, "Scheduling resync because the address has changed."); + ctx.schedule_resync().await?; + } } // the trailing underscore is correct @@ -643,7 +653,7 @@ async fn try_smtp_one_param( }) } else { info!(context, "success: {}", inf); - smtp.disconnect().await; + smtp.disconnect(); Ok(()) } } diff --git a/src/configure/auto_mozilla.rs b/src/configure/auto_mozilla.rs index d53b5a8c5..617429f59 100644 --- a/src/configure/auto_mozilla.rs +++ b/src/configure/auto_mozilla.rs @@ -1,15 +1,15 @@ //! # Thunderbird's Autoconfiguration implementation //! -//! Documentation: +//! Documentation: use std::io::BufRead; use std::str::FromStr; use quick_xml::events::{BytesStart, Event}; -use super::read_url::read_url; use super::{Error, ServerParams}; use crate::context::Context; use crate::login_param::LoginParam; +use crate::net::read_url; use crate::provider::{Protocol, Socket}; #[derive(Debug)] @@ -234,7 +234,7 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result Some(Protocol::Imap), diff --git a/src/configure/auto_outlook.rs b/src/configure/auto_outlook.rs index 8d42fd353..c1cfbe416 100644 --- a/src/configure/auto_outlook.rs +++ b/src/configure/auto_outlook.rs @@ -7,9 +7,9 @@ use std::io::BufRead; use quick_xml::events::Event; -use super::read_url::read_url; use super::{Error, ServerParams}; use crate::context::Context; +use crate::net::read_url; use crate::provider::{Protocol, Socket}; /// Result of parsing a single `Protocol` tag. diff --git a/src/configure/read_url.rs b/src/configure/read_url.rs deleted file mode 100644 index d164a7007..000000000 --- a/src/configure/read_url.rs +++ /dev/null @@ -1,44 +0,0 @@ -use anyhow::{anyhow, format_err}; - -use crate::context::Context; -use crate::socks::Socks5Config; - -pub async fn read_url(context: &Context, url: &str) -> anyhow::Result { - match read_url_inner(context, url).await { - Ok(s) => { - info!(context, "Successfully read url {}", url); - Ok(s) - } - Err(e) => { - info!(context, "Can't read URL {}: {:#}", url, e); - Err(format_err!("Can't read URL {}: {:#}", url, e)) - } - } -} - -pub async fn read_url_inner(context: &Context, url: &str) -> anyhow::Result { - let socks5_config = Socks5Config::from_database(&context.sql).await?; - let client = crate::http::get_client(socks5_config)?; - let mut url = url.to_string(); - - // Follow up to 10 http-redirects - for _i in 0..10 { - let response = client.get(&url).send().await?; - if response.status().is_redirection() { - let headers = response.headers(); - let header = headers - .get_all("location") - .iter() - .last() - .ok_or_else(|| anyhow!("Redirection doesn't have a target location"))? - .to_str()?; - info!(context, "Following redirect to {}", header); - url = header.to_string(); - continue; - } - - return response.text().await.map_err(Into::into); - } - - Err(format_err!("Followed 10 redirections")) -} diff --git a/src/constants.rs b/src/constants.rs index 5ebc47d55..165de5391 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -62,8 +62,15 @@ pub enum MediaQuality { pub enum KeyGenType { #[default] Default = 0, + + /// 2048-bit RSA. Rsa2048 = 1, + + /// [Ed25519](https://ed25519.cr.yp.to/) signature and X25519 encryption. Ed25519 = 2, + + /// 4096-bit RSA. + Rsa4096 = 3, } /// Video chat URL type. @@ -118,7 +125,6 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9); /// Chat type. #[derive( Debug, - Default, Display, Clone, Copy, @@ -134,10 +140,6 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9); )] #[repr(u32)] pub enum Chattype { - /// Undefined chat type. - #[default] - Undefined = 0, - /// 1:1 chat. Single = 100, @@ -192,11 +194,15 @@ pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL; /// How many existing messages shall be fetched after configuration. pub(crate) const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100; +// max. weight of images to send w/o recoding +pub const BALANCED_IMAGE_BYTES: usize = 500_000; +pub const WORSE_IMAGE_BYTES: usize = 130_000; + // max. width/height of an avatar pub(crate) const BALANCED_AVATAR_SIZE: u32 = 256; pub(crate) const WORSE_AVATAR_SIZE: u32 = 128; -// max. width/height of images +// max. width/height of images scaled down because of being too huge pub const BALANCED_IMAGE_SIZE: u32 = 1280; pub const WORSE_IMAGE_SIZE: u32 = 640; @@ -212,8 +218,6 @@ mod tests { #[test] fn test_chattype_values() { // values may be written to disk and must not change - assert_eq!(Chattype::Undefined, Chattype::default()); - assert_eq!(Chattype::Undefined, Chattype::from_i32(0).unwrap()); assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap()); assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap()); assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap()); @@ -227,6 +231,7 @@ mod tests { assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap()); assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap()); assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap()); + assert_eq!(KeyGenType::Rsa4096, KeyGenType::from_i32(3).unwrap()); } #[test] diff --git a/src/contact.rs b/src/contact.rs index 78269f003..fed85e053 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -5,7 +5,7 @@ use std::collections::BinaryHeap; use std::convert::{TryFrom, TryInto}; use std::fmt; use std::ops::Deref; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::{bail, ensure, Context as _, Result}; @@ -25,7 +25,7 @@ use crate::config::Config; use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY}; use crate::context::Context; use crate::events::EventType; -use crate::key::{DcKey, SignedPublicKey}; +use crate::key::{load_self_public_key, DcKey}; use crate::login_param::LoginParam; use crate::message::MessageState; use crate::mimeparser::AvatarAction; @@ -109,7 +109,7 @@ impl ContactId { /// ID of the contact for device messages. pub const DEVICE: ContactId = ContactId::new(5); - const LAST_SPECIAL: ContactId = ContactId::new(9); + pub(crate) const LAST_SPECIAL: ContactId = ContactId::new(9); /// Address to go with [`ContactId::DEVICE`]. /// @@ -139,6 +139,15 @@ impl ContactId { pub const fn to_u32(&self) -> u32 { self.0 } + + /// Mark contact as bot. + pub(crate) async fn mark_bot(&self, context: &Context, is_bot: bool) -> Result<()> { + context + .sql + .execute("UPDATE contacts SET is_bot=? WHERE id=?;", (is_bot, self.0)) + .await?; + Ok(()) + } } impl fmt::Display for ContactId { @@ -223,6 +232,9 @@ pub struct Contact { /// Last seen message signature for this contact, to be displayed in the profile. status: String, + + /// If the contact is a bot. + is_bot: bool, } /// Possible origins of a contact. @@ -338,13 +350,35 @@ impl Default for VerifiedStatus { } impl Contact { - /// Loads a contact snapshot from the database. - pub async fn load_from_db(context: &Context, contact_id: ContactId) -> Result { - let mut contact = context + /// Loads a single contact object from the database. + /// + /// Returns an error if the contact does not exist. + /// + /// For contact ContactId::SELF (1), the function returns sth. + /// like "Me" in the selected language and the email address + /// defined by set_config(). + /// + /// For contact ContactId::DEVICE, the function overrides + /// the contact name and status with localized address. + pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result { + let contact = Self::get_by_id_optional(context, contact_id) + .await? + .with_context(|| format!("contact {contact_id} not found"))?; + Ok(contact) + } + + /// Loads a single contact object from the database. + /// + /// Similar to [`Contact::get_by_id()`] but returns `None` if the contact does not exist. + pub async fn get_by_id_optional( + context: &Context, + contact_id: ContactId, + ) -> Result> { + if let Some(mut contact) = context .sql - .query_row( + .query_row_optional( "SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen, - c.authname, c.param, c.status + c.authname, c.param, c.status, c.is_bot FROM contacts c WHERE c.id=?;", (contact_id,), @@ -357,6 +391,7 @@ impl Contact { let authname: String = row.get(5)?; let param: String = row.get(6)?; let status: Option = row.get(7)?; + let is_bot: bool = row.get(8)?; let contact = Self { id: contact_id, name, @@ -367,27 +402,32 @@ impl Contact { origin, param: param.parse().unwrap_or_default(), status: status.unwrap_or_default(), + is_bot, }; Ok(contact) }, ) - .await?; - if contact_id == ContactId::SELF { - contact.name = stock_str::self_msg(context).await; - contact.addr = context - .get_config(Config::ConfiguredAddr) - .await? - .unwrap_or_default(); - contact.status = context - .get_config(Config::Selfstatus) - .await? - .unwrap_or_default(); - } else if contact_id == ContactId::DEVICE { - contact.name = stock_str::device_messages(context).await; - contact.addr = ContactId::DEVICE_ADDR.to_string(); - contact.status = stock_str::device_messages_hint(context).await; + .await? + { + if contact_id == ContactId::SELF { + contact.name = stock_str::self_msg(context).await; + contact.addr = context + .get_config(Config::ConfiguredAddr) + .await? + .unwrap_or_default(); + contact.status = context + .get_config(Config::Selfstatus) + .await? + .unwrap_or_default(); + } else if contact_id == ContactId::DEVICE { + contact.name = stock_str::device_messages(context).await; + contact.addr = ContactId::DEVICE_ADDR.to_string(); + contact.status = stock_str::device_messages_hint(context).await; + } + Ok(Some(contact)) + } else { + Ok(None) } - Ok(contact) } /// Returns `true` if this contact is blocked. @@ -407,7 +447,13 @@ impl Contact { /// Check if a contact is blocked. pub async fn is_blocked_load(context: &Context, id: ContactId) -> Result { - let blocked = Self::load_from_db(context, id).await?.blocked; + let blocked = context + .sql + .query_row("SELECT blocked FROM contacts WHERE id=?", (id,), |row| { + let blocked: bool = row.get(0)?; + Ok(blocked) + }) + .await?; Ok(blocked) } @@ -466,6 +512,11 @@ impl Contact { Ok(()) } + /// Returns whether contact is a bot. + pub fn is_bot(&self) -> bool { + self.is_bot + } + /// Check if an e-mail address belongs to a known and unblocked contact. /// /// Known and unblocked contacts will be returned by `get_contacts()`. @@ -780,7 +831,11 @@ impl Contact { let mut ret = Vec::new(); let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0; let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0; - + let minimal_origin = if context.get_config_bool(Config::Bot).await? { + Origin::Unknown + } else { + Origin::IncomingReplyTo + }; if flag_verified_only || query.is_some() { let s3str_like_cmd = format!("%{}%", query.unwrap_or("")); context @@ -800,7 +855,7 @@ impl Contact { ), rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![ ContactId::LAST_SPECIAL, - Origin::IncomingReplyTo, + minimal_origin, s3str_like_cmd, s3str_like_cmd, if flag_verified_only { 0i32 } else { 1i32 } @@ -850,10 +905,10 @@ impl Contact { ORDER BY last_seen DESC, id DESC;", sql::repeat_vars(self_addrs.len()) ), - rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![ - ContactId::LAST_SPECIAL, - Origin::IncomingReplyTo - ])), + rusqlite::params_from_iter( + params_iter(&self_addrs) + .chain(params_slice![ContactId::LAST_SPECIAL, minimal_origin]), + ), |row| row.get::<_, ContactId>(0), |ids| { for id in ids { @@ -959,7 +1014,7 @@ impl Contact { ); let mut ret = String::new(); - if let Ok(contact) = Contact::load_from_db(context, contact_id).await { + if let Ok(contact) = Contact::get_by_id(context, contact_id).await { let loginparam = LoginParam::load_configured_params(context).await?; let peerstate = Peerstate::from_addr(context, &contact.addr).await?; @@ -977,7 +1032,7 @@ impl Contact { let finger_prints = stock_str::finger_prints(context).await; ret += &format!("{stock_message}.\n{finger_prints}:"); - let fingerprint_self = SignedPublicKey::load_self(context) + let fingerprint_self = load_self_public_key(context) .await? .fingerprint() .to_string(); @@ -1046,17 +1101,6 @@ impl Contact { Ok(()) } - /// Get a single contact object. For a list, see eg. get_contacts(). - /// - /// For contact ContactId::SELF (1), the function returns sth. - /// like "Me" in the selected language and the email address - /// defined by set_config(). - pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result { - let contact = Contact::load_from_db(context, contact_id).await?; - - Ok(contact) - } - /// Updates `param` column in the database. pub async fn update_param(&self, context: &Context) -> Result<()> { context @@ -1120,11 +1164,29 @@ impl Contact { &self.addr } + /// Get a summary of authorized name and address. + /// + /// The returned string is either "Name (email@domain.com)" or just + /// "email@domain.com" if the name is unset. + /// + /// This string is suitable for sending over email + /// as it does not leak the locally set name. + pub fn get_authname_n_addr(&self) -> String { + if !self.authname.is_empty() { + format!("{} ({})", self.authname, self.addr) + } else { + (&self.addr).into() + } + } + /// Get a summary of name and address. /// /// The returned string is either "Name (email@domain.com)" or just /// "email@domain.com" if the name is unset. /// + /// The result should only be used locally and never sent over the network + /// as it leaks the local contact name. + /// /// The summary is typically used when asking the user something about the contact. /// The attached email address makes the question unique, eg. "Chat with Alan Miller (am@uniquedomain.com)?" pub fn get_name_n_addr(&self) -> String { @@ -1147,7 +1209,7 @@ impl Contact { } } else if let Some(image_rel) = self.param.get(Param::ProfileImage) { if !image_rel.is_empty() { - return Ok(Some(get_abs_path(context, image_rel))); + return Ok(Some(get_abs_path(context, Path::new(image_rel)))); } } Ok(None) @@ -1172,31 +1234,15 @@ impl Contact { /// and if the key has not changed since this verification. /// /// The UI may draw a checkbox or something like that beside verified contacts. - /// pub async fn is_verified(&self, context: &Context) -> Result { - self.is_verified_ex(context, None).await - } - - /// Same as `Contact::is_verified` but allows speeding up things - /// by adding the peerstate belonging to the contact. - /// If you do not have the peerstate available, it is loaded automatically. - pub async fn is_verified_ex( - &self, - context: &Context, - peerstate: Option<&Peerstate>, - ) -> Result { // We're always sort of secured-verified as we could verify the key on this device any time with the key // on this device if self.id == ContactId::SELF { return Ok(VerifiedStatus::BidirectVerified); } - if let Some(peerstate) = peerstate { - if peerstate.verified_key.is_some() { - return Ok(VerifiedStatus::BidirectVerified); - } - } else if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? { - if peerstate.verified_key.is_some() { + if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? { + if peerstate.is_using_verified_key() { return Ok(VerifiedStatus::BidirectVerified); } } @@ -1213,11 +1259,22 @@ impl Contact { /// Returns the ContactId that verified the contact. pub async fn get_verifier_id(&self, context: &Context) -> Result> { - let verifier_addr = self.get_verifier_addr(context).await?; - if let Some(addr) = verifier_addr { - Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?) - } else { - Ok(None) + let Some(verifier_addr) = self.get_verifier_addr(context).await? else { + return Ok(None); + }; + + if verifier_addr == self.addr { + // Contact is directly verified via QR code. + return Ok(Some(ContactId::SELF)); + } + + match Contact::lookup_id_by_addr(context, &verifier_addr, Origin::Unknown).await? { + Some(contact_id) => Ok(Some(contact_id)), + None => { + let addr = &self.addr; + warn!(context, "Could not lookup contact with address {verifier_addr} which introduced {addr}."); + Ok(None) + } } } @@ -1317,7 +1374,7 @@ async fn set_block_contact( contact_id ); - let contact = Contact::load_from_db(context, contact_id).await?; + let contact = Contact::get_by_id(context, contact_id).await?; if contact.blocked != new_blocking { context @@ -1379,7 +1436,7 @@ pub(crate) async fn set_profile_image( profile_image: &AvatarAction, was_encrypted: bool, ) -> Result<()> { - let mut contact = Contact::load_from_db(context, contact_id).await?; + let mut contact = Contact::get_by_id(context, contact_id).await?; let changed = match profile_image { AvatarAction::Change(profile_image) => { if contact_id == ContactId::SELF { @@ -1434,7 +1491,7 @@ pub(crate) async fn set_status( .await?; } } else { - let mut contact = Contact::load_from_db(context, contact_id).await?; + let mut contact = Contact::get_by_id(context, contact_id).await?; if contact.status != status { contact.status = status; @@ -1693,7 +1750,7 @@ mod tests { assert_eq!(may_be_valid_addr("dd.tt"), false); assert_eq!(may_be_valid_addr("tt.dd@uu"), true); assert_eq!(may_be_valid_addr("u@d"), true); - assert_eq!(may_be_valid_addr("u@d."), true); + assert_eq!(may_be_valid_addr("u@d."), false); assert_eq!(may_be_valid_addr("u@d.t"), true); assert_eq!(may_be_valid_addr("u@d.tt"), true); assert_eq!(may_be_valid_addr("u@.tt"), true); @@ -1702,6 +1759,7 @@ mod tests { assert_eq!(may_be_valid_addr("sk <@d.tt>"), false); assert_eq!(may_be_valid_addr("as@sd.de>"), false); assert_eq!(may_be_valid_addr("ask dkl@dd.tt"), false); + assert_eq!(may_be_valid_addr("user@domain.tld."), false); } #[test] @@ -1752,7 +1810,7 @@ mod tests { .await?; assert_ne!(id, ContactId::UNDEFINED); - let contact = Contact::load_from_db(&context.ctx, id).await.unwrap(); + let contact = Contact::get_by_id(&context.ctx, id).await.unwrap(); assert_eq!(contact.get_name(), ""); assert_eq!(contact.get_authname(), "bob"); assert_eq!(contact.get_display_name(), "bob"); @@ -1780,7 +1838,7 @@ mod tests { .await?; assert_eq!(contact_bob_id, id); assert_eq!(modified, Modifier::Modified); - let contact = Contact::load_from_db(&context.ctx, id).await.unwrap(); + let contact = Contact::get_by_id(&context.ctx, id).await.unwrap(); assert_eq!(contact.get_name(), "someone"); assert_eq!(contact.get_authname(), "bob"); assert_eq!(contact.get_display_name(), "someone"); @@ -1846,7 +1904,7 @@ mod tests { .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_id(), contact_id); assert_eq!(contact.get_name(), "Name one"); assert_eq!(contact.get_authname(), "bla foo"); @@ -1865,7 +1923,7 @@ mod tests { .unwrap(); assert_eq!(contact_id, contact_id_test); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_name(), "Real one"); assert_eq!(contact.get_addr(), "one@eins.org"); assert!(!contact.is_blocked()); @@ -1881,7 +1939,7 @@ mod tests { .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::None); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_name(), ""); assert_eq!(contact.get_display_name(), "three@drei.sam"); assert_eq!(contact.get_addr(), "three@drei.sam"); @@ -1898,7 +1956,7 @@ mod tests { .unwrap(); assert_eq!(contact_id, contact_id_test); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_name_n_addr(), "m. serious (three@drei.sam)"); assert!(!contact.is_blocked()); @@ -1913,7 +1971,7 @@ mod tests { .unwrap(); assert_eq!(contact_id, contact_id_test); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "m. serious"); assert_eq!(contact.get_name_n_addr(), "schnucki (three@drei.sam)"); assert!(!contact.is_blocked()); @@ -1929,14 +1987,14 @@ mod tests { .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::None); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_name(), "Wonderland, Alice"); assert_eq!(contact.get_display_name(), "Wonderland, Alice"); assert_eq!(contact.get_addr(), "alice@w.de"); assert_eq!(contact.get_name_n_addr(), "Wonderland, Alice (alice@w.de)"); // check SELF - let contact = Contact::load_from_db(&t, ContactId::SELF).await.unwrap(); + let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap(); assert_eq!(contact.get_name(), stock_str::self_msg(&t).await); assert_eq!(contact.get_addr(), ""); // we're not configured assert!(!contact.is_blocked()); @@ -1967,7 +2025,7 @@ mod tests { assert_eq!(chatlist.len(), 1); let contacts = get_chat_contacts(&t, chat_id).await?; let contact_id = contacts.first().unwrap(); - let contact = Contact::load_from_db(&t, *contact_id).await?; + let contact = Contact::get_by_id(&t, *contact_id).await?; assert_eq!(contact.get_authname(), ""); assert_eq!(contact.get_name(), ""); assert_eq!(contact.get_display_name(), "f@example.org"); @@ -1993,7 +2051,7 @@ mod tests { assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Flobbyfoo"); let chatlist = Chatlist::try_load(&t, 0, Some("flobbyfoo"), None).await?; assert_eq!(chatlist.len(), 1); - let contact = Contact::load_from_db(&t, *contact_id).await?; + let contact = Contact::get_by_id(&t, *contact_id).await?; assert_eq!(contact.get_authname(), "Flobbyfoo"); assert_eq!(contact.get_name(), ""); assert_eq!(contact.get_display_name(), "Flobbyfoo"); @@ -2023,7 +2081,7 @@ mod tests { assert_eq!(chatlist.len(), 0); let chatlist = Chatlist::try_load(&t, 0, Some("Foo Flobby"), None).await?; assert_eq!(chatlist.len(), 1); - let contact = Contact::load_from_db(&t, *contact_id).await?; + let contact = Contact::get_by_id(&t, *contact_id).await?; assert_eq!(contact.get_authname(), "Foo Flobby"); assert_eq!(contact.get_name(), ""); assert_eq!(contact.get_display_name(), "Foo Flobby"); @@ -2041,7 +2099,7 @@ mod tests { assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Falk"); let chatlist = Chatlist::try_load(&t, 0, Some("Falk"), None).await?; assert_eq!(chatlist.len(), 1); - let contact = Contact::load_from_db(&t, *contact_id).await?; + let contact = Contact::get_by_id(&t, *contact_id).await?; assert_eq!(contact.get_authname(), "Foo Flobby"); assert_eq!(contact.get_name(), "Falk"); assert_eq!(contact.get_display_name(), "Falk"); @@ -2080,7 +2138,7 @@ mod tests { // If a contact has ongoing chats, contact is only hidden on deletion Contact::delete(&alice, contact_id).await?; - let contact = Contact::load_from_db(&alice, contact_id).await?; + let contact = Contact::get_by_id(&alice, contact_id).await?; assert_eq!(contact.origin, Origin::Hidden); assert_eq!( Contact::get_all(&alice, 0, Some("bob@example.net")) @@ -2094,7 +2152,7 @@ mod tests { // Can delete contact physically now Contact::delete(&alice, contact_id).await?; - assert!(Contact::load_from_db(&alice, contact_id).await.is_err()); + assert!(Contact::get_by_id(&alice, contact_id).await.is_err()); assert_eq!( Contact::get_all(&alice, 0, Some("bob@example.net")) .await? @@ -2113,7 +2171,7 @@ mod tests { let contact_id1 = Contact::create(&t, "Foo", "foo@bar.de").await?; assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1); Contact::delete(&t, contact_id1).await?; - assert!(Contact::load_from_db(&t, contact_id1).await.is_err()); + assert!(Contact::get_by_id(&t, contact_id1).await.is_err()); assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0); let contact_id2 = Contact::create(&t, "Foo", "foo@bar.de").await?; assert_ne!(contact_id2, contact_id1); @@ -2122,12 +2180,12 @@ mod tests { // test recreation after hiding t.create_chat_with_contact("Foo", "foo@bar.de").await; Contact::delete(&t, contact_id2).await?; - let contact = Contact::load_from_db(&t, contact_id2).await?; + let contact = Contact::get_by_id(&t, contact_id2).await?; assert_eq!(contact.origin, Origin::Hidden); assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0); let contact_id3 = Contact::create(&t, "Foo", "foo@bar.de").await?; - let contact = Contact::load_from_db(&t, contact_id3).await?; + let contact = Contact::get_by_id(&t, contact_id3).await?; assert_eq!(contact.origin, Origin::ManuallyCreated); assert_eq!(contact_id3, contact_id2); assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1); @@ -2150,7 +2208,7 @@ mod tests { .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Created); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "bob1"); assert_eq!(contact.get_name(), ""); assert_eq!(contact.get_display_name(), "bob1"); @@ -2166,7 +2224,7 @@ mod tests { .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "bob2"); assert_eq!(contact.get_name(), ""); assert_eq!(contact.get_display_name(), "bob2"); @@ -2176,7 +2234,7 @@ mod tests { .await .unwrap(); assert!(!contact_id.is_special()); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "bob2"); assert_eq!(contact.get_name(), "bob3"); assert_eq!(contact.get_display_name(), "bob3"); @@ -2192,7 +2250,7 @@ mod tests { .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "bob4"); assert_eq!(contact.get_name(), "bob3"); assert_eq!(contact.get_display_name(), "bob3"); @@ -2205,7 +2263,7 @@ mod tests { // manually create "claire@example.org" without a given name let contact_id = Contact::create(&t, "", "claire@example.org").await.unwrap(); assert!(!contact_id.is_special()); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), ""); assert_eq!(contact.get_name(), ""); assert_eq!(contact.get_display_name(), "claire@example.org"); @@ -2221,7 +2279,7 @@ mod tests { .unwrap(); assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "claire1"); assert_eq!(contact.get_name(), ""); assert_eq!(contact.get_display_name(), "claire1"); @@ -2237,7 +2295,7 @@ mod tests { .unwrap(); assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "claire2"); assert_eq!(contact.get_name(), ""); assert_eq!(contact.get_display_name(), "claire2"); @@ -2260,7 +2318,7 @@ mod tests { ) .await?; assert_eq!(sth_modified, Modifier::Created); - let contact = Contact::load_from_db(&t, contact_id).await?; + let contact = Contact::get_by_id(&t, contact_id).await?; assert_eq!(contact.get_display_name(), "Bob"); // Incoming message from someone else with "Not Bob" in the "To:" field. @@ -2273,7 +2331,7 @@ mod tests { .await?; assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t, contact_id).await?; + let contact = Contact::get_by_id(&t, contact_id).await?; assert_eq!(contact.get_display_name(), "Not Bob"); // Incoming message from Bob, changing the name back. @@ -2286,7 +2344,7 @@ mod tests { .await?; assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); // This was None until the bugfix - let contact = Contact::load_from_db(&t, contact_id).await?; + let contact = Contact::get_by_id(&t, contact_id).await?; assert_eq!(contact.get_display_name(), "Bob"); Ok(()) @@ -2300,7 +2358,7 @@ mod tests { let contact_id = Contact::create(&t, "dave1", "dave@example.org") .await .unwrap(); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), ""); assert_eq!(contact.get_name(), "dave1"); assert_eq!(contact.get_display_name(), "dave1"); @@ -2314,14 +2372,14 @@ mod tests { ) .await .unwrap(); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "dave2"); assert_eq!(contact.get_name(), "dave1"); assert_eq!(contact.get_display_name(), "dave1"); // manually clear the name Contact::create(&t, "", "dave@example.org").await.unwrap(); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "dave2"); assert_eq!(contact.get_name(), ""); assert_eq!(contact.get_display_name(), "dave2"); @@ -2339,21 +2397,21 @@ mod tests { let t = TestContext::new().await; let contact_id = Contact::create(&t, "", "").await.unwrap(); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_name(), ""); assert_eq!(contact.get_addr(), "dave@example.org"); let contact_id = Contact::create(&t, "", "Mueller, Dave ") .await .unwrap(); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_name(), "Mueller, Dave"); assert_eq!(contact.get_addr(), "dave@example.org"); let contact_id = Contact::create(&t, "name1", "name2 ") .await .unwrap(); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); assert_eq!(contact.get_name(), "name1"); assert_eq!(contact.get_addr(), "dave@example.org"); @@ -2597,7 +2655,7 @@ CCCB 5AA9 F6E1 141C 9431 Origin::ManuallyCreated, ) .await?; - let contact = Contact::load_from_db(&alice, contact_id).await?; + let contact = Contact::get_by_id(&alice, contact_id).await?; assert_eq!(contact.last_seen(), 0); let mime = br#"Subject: Hello @@ -2614,7 +2672,7 @@ Hi."#; let timestamp = msg.get_timestamp(); assert!(timestamp > 0); - let contact = Contact::load_from_db(&alice, contact_id).await?; + let contact = Contact::get_by_id(&alice, contact_id).await?; assert_eq!(contact.last_seen(), timestamp); Ok(()) diff --git a/src/context.rs b/src/context.rs index 351a8898f..3298742d4 100644 --- a/src/context.rs +++ b/src/context.rs @@ -6,28 +6,26 @@ use std::future::Future; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::pin::Pin; -use std::sync::atomic::AtomicBool; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::task::Poll; use std::time::{Duration, Instant, SystemTime}; use anyhow::{bail, ensure, Context as _, Result}; -use async_channel::Sender; use ratelimit::Ratelimit; use tokio::sync::{oneshot, Mutex, Notify, RwLock}; -use tokio::task; use crate::chat::{get_chat_cnt, ChatId}; use crate::config::Config; use crate::constants::DC_VERSION_STR; use crate::contact::Contact; -use crate::debug_logging::DebugEventLogData; +use crate::debug_logging::DebugLogging; use crate::events::{Event, EventEmitter, EventType, Events}; -use crate::key::{DcKey, SignedPublicKey}; +use crate::key::{load_self_public_key, DcKey as _}; use crate::login_param::LoginParam; use crate::message::{self, MessageState, MsgId}; use crate::quota::QuotaInfo; -use crate::scheduler::SchedulerState; +use crate::scheduler::{InterruptInfo, SchedulerState}; use crate::sql::Sql; use crate::stock_str::StockStrings; use crate::timesmearing::SmearedTimestamp; @@ -42,7 +40,7 @@ use crate::tools::{duration_to_str, time}; /// /// # Examples /// -/// Creating a new unecrypted database: +/// Creating a new unencrypted database: /// /// ``` /// # let rt = tokio::runtime::Runtime::new().unwrap(); @@ -215,9 +213,6 @@ pub struct InnerContext { /// Set to `None` if quota was never tried to load. pub(crate) quota: RwLock>, - /// Set to true if quota update is requested. - pub(crate) quota_update_request: AtomicBool, - /// IMAP UID resync request. pub(crate) resync_request: AtomicBool, @@ -247,18 +242,10 @@ pub struct InnerContext { pub(crate) last_error: std::sync::RwLock, /// If debug logging is enabled, this contains all necessary information - pub(crate) debug_logging: RwLock>, -} - -#[derive(Debug)] -pub(crate) struct DebugLogging { - /// The message containing the logging xdc - pub(crate) msg_id: MsgId, - /// Handle to the background task responsible for sending - pub(crate) loop_handle: task::JoinHandle<()>, - /// Channel that log events should be sent to. - /// A background loop will receive and handle them. - pub(crate) sender: Sender, + /// + /// Standard RwLock instead of [`tokio::sync::RwLock`] is used + /// because the lock is used from synchronous [`Context::emit_event`]. + pub(crate) debug_logging: std::sync::RwLock>, } /// The state of ongoing process. @@ -268,7 +255,7 @@ enum RunningState { Running { cancel_sender: oneshot::Sender<()> }, /// Cancel signal has been sent, waiting for ongoing process to be freed. - ShallStop, + ShallStop { request: Instant }, /// There is no ongoing process, a new one can be allocated. Stopped, @@ -344,6 +331,12 @@ impl Context { } } + /// Changes encrypted database passphrase. + pub async fn change_passphrase(&self, passphrase: String) -> Result<()> { + self.sql.change_passphrase(passphrase).await?; + Ok(()) + } + /// Returns true if database is open. pub async fn is_open(&self) -> bool { self.sql.is_open().await @@ -388,16 +381,15 @@ impl Context { translated_stockstrings: stockstrings, events, scheduler: SchedulerState::new(), - ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds. + ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6. quota: RwLock::new(None), - quota_update_request: AtomicBool::new(false), resync_request: AtomicBool::new(false), new_msgs_notify, server_id: RwLock::new(None), creation_time: std::time::SystemTime::now(), last_full_folder_scan: Mutex::new(None), last_error: std::sync::RwLock::new("".to_string()), - debug_logging: RwLock::new(None), + debug_logging: std::sync::RwLock::new(None), }; let ctx = Context { @@ -432,6 +424,16 @@ impl Context { self.scheduler.maybe_network().await; } + pub(crate) async fn schedule_resync(&self) -> Result<()> { + self.resync_request.store(true, Ordering::Relaxed); + self.scheduler + .interrupt_inbox(InterruptInfo { + probe_network: false, + }) + .await; + Ok(()) + } + /// Returns a reference to the underlying SQL instance. /// /// Warning: this is only here for testing, not part of the public API. @@ -452,41 +454,18 @@ impl Context { /// Emits a single event. pub fn emit_event(&self, event: EventType) { - if self - .debug_logging - .try_read() - .ok() - .map(|inner| inner.is_some()) - == Some(true) { - self.send_log_event(event.clone()).ok(); - }; + let lock = self.debug_logging.read().expect("RwLock is poisoned"); + if let Some(debug_logging) = &*lock { + debug_logging.log_event(event.clone()); + } + } self.events.emit(Event { id: self.id, typ: event, }); } - pub(crate) fn send_log_event(&self, event: EventType) -> anyhow::Result<()> { - if let Ok(lock) = self.debug_logging.try_read() { - if let Some(DebugLogging { - msg_id: xdc_id, - sender, - .. - }) = &*lock - { - let event_data = DebugEventLogData { - time: time(), - msg_id: *xdc_id, - event, - }; - - sender.try_send(event_data).ok(); - } - } - Ok(()) - } - /// Emits a generic MsgsChanged event (without chat or message id) pub fn emit_msgs_changed_without_ids(&self) { self.emit_event(EventType::MsgsChanged { @@ -561,14 +540,19 @@ impl Context { let mut s = self.running_state.write().await; // Take out the state so we can call the oneshot sender (which takes ownership). - let current_state = std::mem::replace(&mut *s, RunningState::ShallStop); + let current_state = std::mem::replace( + &mut *s, + RunningState::ShallStop { + request: Instant::now(), + }, + ); match current_state { RunningState::Running { cancel_sender } => match cancel_sender.send(()) { Ok(()) => info!(self, "Signaling the ongoing process to stop ASAP."), Err(()) => warn!(self, "could not cancel ongoing"), }, - RunningState::ShallStop | RunningState::Stopped => { + RunningState::ShallStop { .. } | RunningState::Stopped => { // Put back the current state *s = current_state; info!(self, "No ongoing process to stop.",); @@ -580,7 +564,7 @@ impl Context { pub(crate) async fn shall_stop_ongoing(&self) -> bool { match &*self.running_state.read().await { RunningState::Running { .. } => false, - RunningState::ShallStop | RunningState::Stopped => true, + RunningState::ShallStop { .. } | RunningState::Stopped => true, } } @@ -615,6 +599,7 @@ impl Context { let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?; let bcc_self = self.get_config_int(Config::BccSelf).await?; let send_sync_msgs = self.get_config_int(Config::SendSyncMsgs).await?; + let disable_idle = self.get_config_bool(Config::DisableIdle).await?; let prv_key_cnt = self.sql.count("SELECT COUNT(*) FROM keypairs;", ()).await?; @@ -622,7 +607,7 @@ impl Context { .sql .count("SELECT COUNT(*) FROM acpeerstates;", ()) .await?; - let fingerprint_str = match SignedPublicKey::load_self(self).await { + let fingerprint_str = match load_self_public_key(self).await { Ok(key) => key.fingerprint().hex(), Err(err) => format!(""), }; @@ -727,6 +712,7 @@ impl Context { ); res.insert("bcc_self", bcc_self.to_string()); res.insert("send_sync_msgs", send_sync_msgs.to_string()); + res.insert("disable_idle", disable_idle.to_string()); res.insert("private_key_count", prv_key_cnt.to_string()); res.insert("public_key_count", pub_key_cnt.to_string()); res.insert("fingerprint", fingerprint_str); @@ -788,7 +774,6 @@ impl Context { .await? .to_string(), ); - res.insert( "debug_logging", self.get_config_int(Config::DebugLogging).await?.to_string(), @@ -797,6 +782,16 @@ impl Context { "last_msg_id", self.get_config_int(Config::LastMsgId).await?.to_string(), ); + res.insert( + "gossip_period", + self.get_config_int(Config::GossipPeriod).await?.to_string(), + ); + res.insert( + "verified_one_on_one_chats", + self.get_config_bool(Config::VerifiedOneOnOneChats) + .await? + .to_string(), + ); let elapsed = self.creation_time.elapsed(); res.insert("uptime", duration_to_str(elapsed.unwrap_or_default())); @@ -850,7 +845,22 @@ impl Context { pub async fn get_next_msgs(&self) -> Result> { let last_msg_id = match self.get_config(Config::LastMsgId).await? { Some(s) => MsgId::new(s.parse()?), - None => MsgId::new_unset(), + None => { + // If `last_msg_id` is not set yet, + // subtract 1 from the last id, + // so a single message is returned and can + // be marked as seen. + self.sql + .query_row( + "SELECT IFNULL((SELECT MAX(id) - 1 FROM msgs), 0)", + (), + |row| { + let msg_id: MsgId = row.get(0)?; + Ok(msg_id) + }, + ) + .await? + } }; let list = self @@ -1132,7 +1142,7 @@ mod tests { async fn receive_msg(t: &TestContext, chat: &Chat) { let members = get_chat_contacts(t, chat.id).await.unwrap(); - let contact = Contact::load_from_db(t, *members.first().unwrap()) + let contact = Contact::get_by_id(t, *members.first().unwrap()) .await .unwrap(); let msg = format!( @@ -1394,11 +1404,11 @@ mod tests { // Add messages to chat with Bob. let mut msg1 = Message::new(Viewtype::Text); - msg1.set_text(Some("foobar".to_string())); + msg1.set_text("foobar".to_string()); send_msg(&alice, chat.id, &mut msg1).await?; let mut msg2 = Message::new(Viewtype::Text); - msg2.set_text(Some("barbaz".to_string())); + msg2.set_text("barbaz".to_string()); send_msg(&alice, chat.id, &mut msg2).await?; // Global search with a part of text finds the message. @@ -1494,7 +1504,7 @@ mod tests { // Add 999 messages let mut msg = Message::new(Viewtype::Text); - msg.set_text(Some("foobar".to_string())); + msg.set_text("foobar".to_string()); for _ in 0..999 { send_msg(&alice, chat.id, &mut msg).await?; } @@ -1543,6 +1553,35 @@ mod tests { Ok(()) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_context_change_passphrase() -> Result<()> { + let dir = tempdir()?; + let dbfile = dir.path().join("db.sqlite"); + + let id = 1; + let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new()) + .await + .context("failed to create context")?; + assert_eq!(context.open("foo".to_string()).await?, true); + assert_eq!(context.is_open().await, true); + + context + .set_config(Config::Addr, Some("alice@example.org")) + .await?; + + context + .change_passphrase("bar".to_string()) + .await + .context("Failed to change passphrase")?; + + assert_eq!( + context.get_config(Config::Addr).await?.unwrap(), + "alice@example.org" + ); + + Ok(()) + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ongoing() -> Result<()> { let context = TestContext::new().await; diff --git a/src/debug_logging.rs b/src/debug_logging.rs index d7c93f0e4..d9a67473e 100644 --- a/src/debug_logging.rs +++ b/src/debug_logging.rs @@ -1,18 +1,40 @@ //! Forward log messages to logging webxdc -use crate::{ - chat::ChatId, - config::Config, - context::{Context, DebugLogging}, - message::{Message, MsgId, Viewtype}, - param::Param, - webxdc::StatusUpdateItem, - Event, EventType, -}; -use async_channel::{self as channel, Receiver}; +use crate::chat::ChatId; +use crate::config::Config; +use crate::context::Context; +use crate::events::EventType; +use crate::message::{Message, MsgId, Viewtype}; +use crate::param::Param; +use crate::tools::time; +use crate::webxdc::StatusUpdateItem; +use async_channel::{self as channel, Receiver, Sender}; use serde_json::json; use std::path::PathBuf; use tokio::task; +#[derive(Debug)] +pub(crate) struct DebugLogging { + /// The message containing the logging xdc + pub(crate) msg_id: MsgId, + /// Handle to the background task responsible for sending + pub(crate) loop_handle: task::JoinHandle<()>, + /// Channel that log events should be sent to. + /// A background loop will receive and handle them. + pub(crate) sender: Sender, +} + +impl DebugLogging { + pub(crate) fn log_event(&self, event: EventType) { + let event_data = DebugEventLogData { + time: time(), + msg_id: self.msg_id, + event, + }; + + self.sender.try_send(event_data).ok(); + } +} + /// Store all information needed to log an event to a webxdc. pub struct DebugEventLogData { pub time: i64, @@ -48,12 +70,9 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver { - context.events.emit(Event { - id: context.id, - typ: EventType::WebxdcStatusUpdate { - msg_id, - status_update_serial: serial, - }, + context.emit_event(EventType::WebxdcStatusUpdate { + msg_id, + status_update_serial: serial, }); } } @@ -112,24 +131,26 @@ pub(crate) async fn set_debug_logging_xdc(ctx: &Context, id: Option) -> a Some(msg_id.to_string().as_ref()), ) .await?; - let debug_logging = &mut *ctx.debug_logging.write().await; - match debug_logging { - // Switch logging xdc - Some(debug_logging) => debug_logging.msg_id = msg_id, - // Bootstrap background loop for message forwarding - None => { - let (sender, debug_logging_recv) = channel::bounded(1000); - let loop_handle = { - let ctx = ctx.clone(); - task::spawn( - async move { debug_logging_loop(&ctx, debug_logging_recv).await }, - ) - }; - *debug_logging = Some(DebugLogging { - msg_id, - loop_handle, - sender, - }); + { + let debug_logging = &mut *ctx.debug_logging.write().expect("RwLock is poisoned"); + match debug_logging { + // Switch logging xdc + Some(debug_logging) => debug_logging.msg_id = msg_id, + // Bootstrap background loop for message forwarding + None => { + let (sender, debug_logging_recv) = channel::bounded(1000); + let loop_handle = { + let ctx = ctx.clone(); + task::spawn(async move { + debug_logging_loop(&ctx, debug_logging_recv).await + }) + }; + *debug_logging = Some(DebugLogging { + msg_id, + loop_handle, + sender, + }); + } } } info!(ctx, "replacing logging webxdc"); @@ -139,7 +160,7 @@ pub(crate) async fn set_debug_logging_xdc(ctx: &Context, id: Option) -> a ctx.sql .set_raw_config(Config::DebugLogging.as_ref(), None) .await?; - *ctx.debug_logging.write().await = None; + *ctx.debug_logging.write().expect("RwLock is poisoned") = None; info!(ctx, "removing logging webxdc"); } } diff --git a/src/decrypt.rs b/src/decrypt.rs index 1165e3dd7..24ab873f0 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -13,7 +13,6 @@ use crate::contact::addr_cmp; use crate::context::Context; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey}; -use crate::keyring::Keyring; use crate::peerstate::Peerstate; use crate::pgp; @@ -26,17 +25,33 @@ use crate::pgp; pub fn try_decrypt( context: &Context, mail: &ParsedMail<'_>, - private_keyring: &Keyring, - public_keyring_for_validate: &Keyring, + private_keyring: &[SignedSecretKey], + public_keyring_for_validate: &[SignedPublicKey], ) -> Result, HashSet)>> { - let encrypted_data_part = match get_autocrypt_mime(mail) - .or_else(|| get_mixed_up_mime(mail)) - .or_else(|| get_attachment_mime(mail)) - { + let encrypted_data_part = match { + let mime = get_autocrypt_mime(mail); + if mime.is_some() { + info!(context, "Detected Autocrypt-mime message."); + } + mime + } + .or_else(|| { + let mime = get_mixed_up_mime(mail); + if mime.is_some() { + info!(context, "Detected mixed-up mime message."); + } + mime + }) + .or_else(|| { + let mime = get_attachment_mime(mail); + if mime.is_some() { + info!(context, "Detected attached Autocrypt-mime message."); + } + mime + }) { None => return Ok(None), Some(res) => res, }; - info!(context, "Detected Autocrypt-mime message"); decrypt_part( encrypted_data_part, @@ -211,8 +226,8 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail /// Returns Ok(None) if nothing encrypted was found. fn decrypt_part( mail: &ParsedMail<'_>, - private_keyring: &Keyring, - public_keyring_for_validate: &Keyring, + private_keyring: &[SignedSecretKey], + public_keyring_for_validate: &[SignedPublicKey], ) -> Result, HashSet)>> { let data = mail.get_body_raw()?; @@ -247,7 +262,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool { /// Returns None if the message is not Multipart/Signed or doesn't contain necessary parts. pub(crate) fn validate_detached_signature<'a, 'b>( mail: &'a ParsedMail<'b>, - public_keyring_for_validate: &Keyring, + public_keyring_for_validate: &[SignedPublicKey], ) -> Option<(&'a ParsedMail<'b>, HashSet)> { if mail.ctype.mimetype != "multipart/signed" { return None; @@ -267,13 +282,13 @@ pub(crate) fn validate_detached_signature<'a, 'b>( } } -pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Keyring { - let mut public_keyring_for_validate: Keyring = Keyring::new(); +pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Vec { + let mut public_keyring_for_validate = Vec::new(); if let Some(peerstate) = peerstate { if let Some(key) = &peerstate.public_key { - public_keyring_for_validate.add(key.clone()); + public_keyring_for_validate.push(key.clone()); } else if let Some(key) = &peerstate.gossip_key { - public_keyring_for_validate.add(key.clone()); + public_keyring_for_validate.push(key.clone()); } } public_keyring_for_validate @@ -399,8 +414,22 @@ mod tests { let bob = TestContext::new_bob().await; receive_imf(&bob, attachment_mime, false).await?; let msg = bob.get_last_msg().await; - assert_eq!(msg.text.as_deref(), Some("Hello from Thunderbird!")); + assert_eq!(msg.text, "Hello from Thunderbird!"); Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_mixed_up_mime_long() -> Result<()> { + // Long "mixed-up" mail as received when sending an encrypted message using Delta Chat + // Desktop via MS Exchange (actually made with TB though). + let mixed_up_mime = include_bytes!("../test-data/message/mixed-up-long.eml"); + let bob = TestContext::new_bob().await; + receive_imf(&bob, mixed_up_mime, false).await?; + let msg = bob.get_last_msg().await; + assert!(!msg.get_text().is_empty()); + assert!(msg.has_html()); + assert!(msg.id.get_html(&bob).await?.unwrap().len() > 40000); + Ok(()) + } } diff --git a/src/dehtml.rs b/src/dehtml.rs index 9f786a71a..02dbea4e7 100644 --- a/src/dehtml.rs +++ b/src/dehtml.rs @@ -10,10 +10,11 @@ use quick_xml::{ Reader, }; -static LINE_RE: Lazy = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap()); +use crate::simplify::{simplify_quote, SimplifiedText}; struct Dehtml { strbuilder: String, + quote: String, add_text: AddText, last_href: Option, /// GMX wraps a quote in `
`. After a `
`, this count is @@ -29,17 +30,22 @@ struct Dehtml { } impl Dehtml { - fn line_prefix(&self) -> &str { - if self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0 { - "> " + /// Returns true if HTML parser is currently inside the quote. + fn is_quote(&self) -> bool { + self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0 + } + + /// Returns the buffer where the text should be written. + /// + /// If the parser is inside the quote, returns the quote buffer. + fn get_buf(&mut self) -> &mut String { + if self.is_quote() { + &mut self.quote } else { - "" + &mut self.strbuilder } } - fn append_prefix(&self, line_end: &str) -> String { - // line_end is e.g. "\n\n". We add "> " if necessary. - line_end.to_string() + self.line_prefix() - } + fn get_add_text(&self) -> AddText { if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 { AddText::No // Everything between `
` and `
` is metadata which we don't want @@ -51,30 +57,70 @@ impl Dehtml { #[derive(Debug, PartialEq, Clone, Copy)] enum AddText { + /// Inside `