mirror of
https://github.com/chatmail/core.git
synced 2026-10-11 07:30:43 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
974e32dd76 |
@@ -15,6 +15,6 @@ updates:
|
|||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "monthly"
|
interval: "weekly"
|
||||||
cooldown:
|
cooldown:
|
||||||
default-days: 7
|
default-days: 7
|
||||||
|
|||||||
+38
-25
@@ -20,7 +20,7 @@ permissions: {}
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: -Dwarnings
|
RUSTFLAGS: -Dwarnings
|
||||||
RUST_VERSION: 1.99.0
|
RUST_VERSION: 1.95.0
|
||||||
|
|
||||||
# Minimum Supported Rust Version
|
# Minimum Supported Rust Version
|
||||||
MSRV: 1.89.0
|
MSRV: 1.89.0
|
||||||
@@ -30,11 +30,8 @@ jobs:
|
|||||||
name: Lint Rust
|
name: Lint Rust
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
env:
|
|
||||||
# Tests always unwind: match it so dependencies are only checked once.
|
|
||||||
CARGO_PROFILE_DEV_PANIC: unwind
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -43,7 +40,7 @@ jobs:
|
|||||||
- run: rustup override set $RUST_VERSION
|
- run: rustup override set $RUST_VERSION
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-bin: false
|
cache-bin: false
|
||||||
@@ -51,6 +48,8 @@ jobs:
|
|||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
run: scripts/clippy.sh
|
run: scripts/clippy.sh
|
||||||
|
- name: Check with all features
|
||||||
|
run: cargo check --workspace --all-targets --all-features
|
||||||
- name: Check with only default features
|
- name: Check with only default features
|
||||||
run: cargo check --all-targets
|
run: cargo check --all-targets
|
||||||
|
|
||||||
@@ -59,16 +58,30 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25
|
- uses: EmbarkStudios/cargo-deny-action@a531616d8ce3b9177443e48a1159bc945a099823
|
||||||
with:
|
with:
|
||||||
arguments: --workspace --all-features --locked
|
arguments: --workspace --all-features --locked
|
||||||
command: check
|
command: check
|
||||||
command-arguments: "-Dwarnings"
|
command-arguments: "-Dwarnings"
|
||||||
|
|
||||||
|
provider_database:
|
||||||
|
name: Check provider database
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Install rustfmt
|
||||||
|
run: rustup component add --toolchain stable-x86_64-unknown-linux-gnu rustfmt
|
||||||
|
- name: Check provider database
|
||||||
|
run: scripts/update-provider-database.sh
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
name: Rust doc comments
|
name: Rust doc comments
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -76,12 +89,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
RUSTDOCFLAGS: -Dwarnings
|
RUSTDOCFLAGS: -Dwarnings
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-bin: false
|
cache-bin: false
|
||||||
@@ -115,7 +128,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
if: matrix.rust == 'latest'
|
if: matrix.rust == 'latest'
|
||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -127,25 +140,25 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-bin: false
|
cache-bin: false
|
||||||
|
|
||||||
- name: Install nextest
|
- name: Install nextest
|
||||||
uses: taiki-e/install-action@9983c65e42da123ff25d1f78505eb6de315aa172
|
uses: taiki-e/install-action@60ae4ce63c7aeb6e96d7f572c1ec7fafbb17ca80
|
||||||
with:
|
with:
|
||||||
tool: nextest
|
tool: nextest
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
env:
|
env:
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
run: cargo nextest run --workspace --exclude deltachat-jsonrpc-bindings --locked
|
run: cargo nextest run --workspace --locked
|
||||||
|
|
||||||
- name: Doc-Tests
|
- name: Doc-Tests
|
||||||
env:
|
env:
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
run: cargo test --workspace --exclude deltachat-jsonrpc-bindings --locked --doc
|
run: cargo test --workspace --locked --doc
|
||||||
|
|
||||||
- name: Test cargo vendor
|
- name: Test cargo vendor
|
||||||
run: cargo vendor
|
run: cargo vendor
|
||||||
@@ -158,13 +171,13 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-bin: false
|
cache-bin: false
|
||||||
@@ -187,13 +200,13 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-bin: false
|
cache-bin: false
|
||||||
@@ -213,7 +226,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -239,7 +252,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -289,7 +302,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -301,7 +314,7 @@ jobs:
|
|||||||
path: target/debug
|
path: target/debug
|
||||||
|
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v7.0.0
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
|
||||||
@@ -343,13 +356,13 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v7.0.0
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ jobs:
|
|||||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server binaries
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||||
@@ -54,11 +54,11 @@ jobs:
|
|||||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server wheels
|
- name: Build deltachat-rpc-server wheels
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||||
@@ -78,11 +78,11 @@ jobs:
|
|||||||
arch: [win32, win64]
|
arch: [win32, win64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server binaries
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||||
@@ -102,11 +102,11 @@ jobs:
|
|||||||
arch: [win32, win64]
|
arch: [win32, win64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server wheels
|
- name: Build deltachat-rpc-server wheels
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||||
@@ -127,7 +127,7 @@ jobs:
|
|||||||
|
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -153,11 +153,11 @@ jobs:
|
|||||||
arch: [arm64-v8a, armeabi-v7a]
|
arch: [arm64-v8a, armeabi-v7a]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server binaries
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||||
@@ -177,11 +177,11 @@ jobs:
|
|||||||
arch: [arm64-v8a, armeabi-v7a]
|
arch: [arm64-v8a, armeabi-v7a]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server wheels
|
- name: Build deltachat-rpc-server wheels
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||||
@@ -204,11 +204,11 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||||
|
|
||||||
- name: Download Linux aarch64 binary
|
- name: Download Linux aarch64 binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v7
|
||||||
@@ -370,14 +370,6 @@ jobs:
|
|||||||
- name: List artifacts
|
- name: List artifacts
|
||||||
run: ls -l dist/
|
run: ls -l dist/
|
||||||
|
|
||||||
- name: Check that the wheel metadata reads back
|
|
||||||
run: |
|
|
||||||
echo 'You can check a local `nix build` on non-Mac machines against the following checksums.'
|
|
||||||
sha256sum dist/*.whl
|
|
||||||
mkdir tagcheck
|
|
||||||
cp dist/*.whl tagcheck/
|
|
||||||
nix run --inputs-from . nixpkgs#python3Packages.wheel -- tags tagcheck/*.whl
|
|
||||||
|
|
||||||
- name: Upload binaries to the GitHub release
|
- name: Upload binaries to the GitHub release
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
env:
|
env:
|
||||||
@@ -390,7 +382,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish deltachat-rpc-server to PyPI
|
- name: Publish deltachat-rpc-server to PyPI
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||||
|
|
||||||
publish_npm_package:
|
publish_npm_package:
|
||||||
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
||||||
@@ -405,11 +397,11 @@ jobs:
|
|||||||
# Needed to publish the binaries to the release.
|
# Needed to publish the binaries to the release.
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-python@v7.0.0
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
@@ -521,12 +513,15 @@ jobs:
|
|||||||
deltachat-rpc-server/npm-package/*.tgz
|
deltachat-rpc-server/npm-package/*.tgz
|
||||||
|
|
||||||
# Configure Node.js for publishing.
|
# Configure Node.js for publishing.
|
||||||
# Check <https://docs.npmjs.com/trusted-publishers> for the version requirements.
|
- uses: actions/setup-node@v6
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 20
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
package-manager-cache: false # never use caching in release builds
|
|
||||||
|
# Ensure npm 11.5.1 or later is installed.
|
||||||
|
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
||||||
|
- name: Update npm
|
||||||
|
run: npm install -g npm@latest
|
||||||
|
|
||||||
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
name: Check that current version ends with -dev
|
name: Check that current version ends with -dev
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|||||||
@@ -17,29 +17,31 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
# Configure Node.js for publishing.
|
- uses: actions/setup-node@v6
|
||||||
# Check <https://docs.npmjs.com/trusted-publishers> for the version requirements.
|
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 20
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
package-manager-cache: false # never use caching in release builds
|
|
||||||
|
# Ensure npm 11.5.1 or later is installed.
|
||||||
|
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
||||||
|
- name: Update npm
|
||||||
|
run: npm install -g npm@latest
|
||||||
|
|
||||||
- name: Install dependencies without running scripts
|
- name: Install dependencies without running scripts
|
||||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm install --ignore-scripts
|
run: npm install --ignore-scripts
|
||||||
|
|
||||||
- name: Package
|
- name: Package
|
||||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: |
|
run: |
|
||||||
npm run build
|
npm run build
|
||||||
npm pack .
|
npm pack .
|
||||||
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
||||||
|
|||||||
@@ -16,30 +16,30 @@ jobs:
|
|||||||
build_and_test:
|
build_and_test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 18.x
|
||||||
- name: Add Rust cache
|
- name: Add Rust cache
|
||||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
cache-bin: false
|
cache-bin: false
|
||||||
- name: npm install
|
- name: npm install
|
||||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm install
|
run: npm install
|
||||||
- name: Build TypeScript, run Rust tests, generate bindings
|
- name: Build TypeScript, run Rust tests, generate bindings
|
||||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm run build
|
run: npm run build
|
||||||
- name: Run integration tests
|
- name: Run integration tests
|
||||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm run test
|
run: npm run test
|
||||||
env:
|
env:
|
||||||
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm run prettier:check
|
run: npm run prettier:check
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- flake.nix
|
- flake.nix
|
||||||
- flake.lock
|
- flake.lock
|
||||||
- nix/**
|
|
||||||
- .github/workflows/nix.yml
|
- .github/workflows/nix.yml
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- flake.nix
|
- flake.nix
|
||||||
- flake.lock
|
- flake.lock
|
||||||
- nix/**
|
|
||||||
- .github/workflows/nix.yml
|
- .github/workflows/nix.yml
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -23,12 +21,12 @@ jobs:
|
|||||||
name: check flake formatting
|
name: check flake formatting
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||||
- run: nix fmt flake.nix nix/ -- --check
|
- run: nix fmt flake.nix -- --check
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: nix build
|
name: nix build
|
||||||
@@ -81,11 +79,11 @@ jobs:
|
|||||||
#- deltachat-rpc-server-x86_64-android
|
#- deltachat-rpc-server-x86_64-android
|
||||||
#- deltachat-rpc-server-x86-android
|
#- deltachat-rpc-server-x86-android
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||||
- run: nix build .#${{ matrix.installable }}
|
- run: nix build .#${{ matrix.installable }}
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
@@ -102,9 +100,9 @@ jobs:
|
|||||||
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
|
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
|
||||||
# - deltachat-rpc-server-aarch64-darwin
|
# - deltachat-rpc-server-aarch64-darwin
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||||
- run: nix build .#${{ matrix.installable }}
|
- run: nix build .#${{ matrix.installable }}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -47,4 +47,4 @@ jobs:
|
|||||||
name: python-package-distributions
|
name: python-package-distributions
|
||||||
path: dist/
|
path: dist/
|
||||||
- name: Publish deltachat-rpc-client to PyPI
|
- name: Publish deltachat-rpc-client to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ jobs:
|
|||||||
name: Build REPL example
|
name: Build REPL example
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||||
- name: Build
|
- name: Build
|
||||||
run: nix build .#deltachat-repl-win64
|
run: nix build .#deltachat-repl-win64
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
url: https://rs.delta.chat/
|
url: https://rs.delta.chat/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.RS_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.RS_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||||
chmod 600 "$HOME/.ssh/key"
|
chmod 600 "$HOME/.ssh/key"
|
||||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.RS_DOCS_SSH_USER }}@rs.delta.chat:"
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.RS_DOCS_SSH_USER }}@rs.delta.chat:/var/www/html/rs.delta.chat/"
|
||||||
|
|
||||||
build-python:
|
build-python:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -36,12 +36,12 @@ jobs:
|
|||||||
url: https://py.delta.chat/
|
url: https://py.delta.chat/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||||
- name: Build Python documentation
|
- name: Build Python documentation
|
||||||
run: nix build .#python-docs
|
run: nix build .#python-docs
|
||||||
- name: Upload to py.delta.chat
|
- name: Upload to py.delta.chat
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.PY_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.PY_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||||
chmod 600 "$HOME/.ssh/key"
|
chmod 600 "$HOME/.ssh/key"
|
||||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.PY_DOCS_SSH_USER }}@py.delta.chat:"
|
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.PY_DOCS_SSH_USER }}@py.delta.chat:/var/www/html/py.delta.chat"
|
||||||
|
|
||||||
build-c:
|
build-c:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -58,12 +58,12 @@ jobs:
|
|||||||
url: https://c.delta.chat/
|
url: https://c.delta.chat/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||||
- name: Build C documentation
|
- name: Build C documentation
|
||||||
run: nix build .#docs
|
run: nix build .#docs
|
||||||
- name: Upload to c.delta.chat
|
- name: Upload to c.delta.chat
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.C_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.C_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||||
chmod 600 "$HOME/.ssh/key"
|
chmod 600 "$HOME/.ssh/key"
|
||||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.C_DOCS_SSH_USER }}@c.delta.chat:"
|
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.C_DOCS_SSH_USER }}@c.delta.chat:/var/www/html/c.delta.chat"
|
||||||
|
|
||||||
build-ts:
|
build-ts:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -81,18 +81,18 @@ jobs:
|
|||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./deltachat-jsonrpc-bindings/typescript
|
working-directory: ./deltachat-jsonrpc/typescript
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: '18'
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: npm install
|
run: npm install
|
||||||
- name: npm run build
|
- name: npm run build
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||||
chmod 600 "$HOME/.ssh/key"
|
chmod 600 "$HOME/.ssh/key"
|
||||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc-bindings/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:"
|
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:/var/www/html/js.jsonrpc.delta.chat/"
|
||||||
|
|
||||||
build-cffi:
|
build-cffi:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
url: https://cffi.delta.chat/
|
url: https://cffi.delta.chat/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -125,4 +125,4 @@ jobs:
|
|||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.CFFI_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.CFFI_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||||
chmod 600 "$HOME/.ssh/key"
|
chmod 600 "$HOME/.ssh/key"
|
||||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.CFFI_DOCS_SSH_USER }}@delta.chat:"
|
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.CFFI_DOCS_SSH_USER }}@delta.chat:/var/www/html/cffi.delta.chat/"
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run zizmor
|
- name: Run zizmor
|
||||||
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
|
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||||
|
|||||||
-686
@@ -1,679 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [2.63.0] - 2026-10-09
|
|
||||||
|
|
||||||
### API-Changes
|
|
||||||
|
|
||||||
- add `PinnedMessagesChanged` event.
|
|
||||||
- [**breaking**]: remove unused `dc_chatlist_get_summary2()` ([#8812](https://github.com/chatmail/core/pull/8812))
|
|
||||||
- new `dc_msg_get_summary_text()` ([#8813](https://github.com/chatmail/core/pull/8813)).
|
|
||||||
|
|
||||||
### Features / Changes
|
|
||||||
|
|
||||||
- connect to the most recently successfully used SMTP transport first.
|
|
||||||
- re-add "second device added" message if deleted soon after transfer ([#8758](https://github.com/chatmail/core/pull/8758)).
|
|
||||||
- do not request MDNs for unencrypted messages.
|
|
||||||
- do not send unencrypted MDNs.
|
|
||||||
- add context to HTTP(S) proxy connection errors.
|
|
||||||
- deltachat-repl: remove "reset" command.
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- Correctly percent-encode addresses in securejoin invite codes ([#8747](https://github.com/chatmail/core/pull/8747)).
|
|
||||||
- regenerate deltachat.pc if env changed ([#8752](https://github.com/chatmail/core/pull/8752)).
|
|
||||||
- delete handled securejoin messages on all relays.
|
|
||||||
- add headers to unencrypted MDNs.
|
|
||||||
- In SMTP loop, reset `transport_id` and `from` when disconnecting ([#8797](https://github.com/chatmail/core/pull/8797)).
|
|
||||||
- deltachat-rpc-client: shutdown all threads after RPC server crash.
|
|
||||||
- (linux-only) make sure large attachments return memory to kernel.
|
|
||||||
- do not reencode animated WebPs into JPEG.
|
|
||||||
|
|
||||||
### CI
|
|
||||||
|
|
||||||
- update Rust to 1.99.0.
|
|
||||||
- speedup lint job and the Rust test builds
|
|
||||||
|
|
||||||
### Refactor
|
|
||||||
|
|
||||||
- [**breaking**] stop saving legacy config values when configuring a transport.
|
|
||||||
- core does not set deprecated config values `addr`, `mail_pw` etc. Use `list_transports()` API instead.
|
|
||||||
- get rid of dead sticker-related code in check_or_recode_to_size().
|
|
||||||
- factor add_headers_to_part() out of mimefactory::add_headers_to_protected_part().
|
|
||||||
- replace MimeFactory::from_mdn() with a standalone mimefactory::mdn().
|
|
||||||
- take securejoin addresses from transport list.
|
|
||||||
- deltachat-repl: move all commands to cmdline.rs.
|
|
||||||
- do not guess image format from file extension on I/O error.
|
|
||||||
- reduce macro-generated lines by >70%, and drop tracing.
|
|
||||||
- stop excluding `COPYUID` responses from the logs.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
- move pinned messages tests into pinned_messages_tests module.
|
|
||||||
- Add basic tests for rendering unencrypted messages ([#8736](https://github.com/chatmail/core/pull/8736)).
|
|
||||||
- do not ignore errors in add_pseudo_transport().
|
|
||||||
- test sending and receiving long filename with emojis.
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- update rPGP from 0.20.0 to 0.21.0.
|
|
||||||
- reduce noise created by key-contact migration.
|
|
||||||
- enable `clippy::unnecessary_wraps`, fix `clippy::redundant_clone` and `clippy::string_lit_as_bytes` suggestions.
|
|
||||||
- add chatmail.au.
|
|
||||||
- add chatmail.cc ([#8756](https://github.com/chatmail/core/pull/8756)).
|
|
||||||
- remove some `unwrap()` calls.
|
|
||||||
- remove unused "sdp" dependency.
|
|
||||||
- cargo: bump smallvec from 1.15.2 to 1.16.1.
|
|
||||||
- cargo: bump syn from 3.0.4 to 3.0.6.
|
|
||||||
- deps: bump zizmorcore/zizmor-action from 0.6.2 to 0.6.4.
|
|
||||||
- cargo: bump mailparse from 0.16.1 to 0.17.0.
|
|
||||||
- cargo: bump uuid from 1.25.0 to 1.26.1.
|
|
||||||
- deps: bump cachix/install-nix-action from 31.11.0 to 31.11.1.
|
|
||||||
- cargo: bump hyper from 1.10.1 to 1.11.1.
|
|
||||||
- cargo: bump tokio-rustls from 0.26.4 to 0.26.5.
|
|
||||||
- cargo: bump thiserror from 2.0.20 to 2.0.21.
|
|
||||||
- deps: bump taiki-e/install-action from 2.86.7 to 2.87.20.
|
|
||||||
- cargo: bump hyper-util from 0.1.20 to 0.1.21.
|
|
||||||
- cargo: bump dirs from 6.0.0 to 7.0.0.
|
|
||||||
- move deltachat-time to dev dependencies.
|
|
||||||
- update astral-tokio-tar from 0.6.4 to 0.7.0.
|
|
||||||
- update async-imap to 0.12.0.
|
|
||||||
- update mail-builder to 1.0.
|
|
||||||
|
|
||||||
## [2.62.0] - 2026-09-22
|
|
||||||
|
|
||||||
### API-Changes
|
|
||||||
|
|
||||||
- Remove unused ImapMessageMoved event.
|
|
||||||
- [**breaking**] get rid of `Qr::FprWithoutAddr` and `Qr::FprMismatch` variants.
|
|
||||||
- removed `DC_QR_FPR_WITHOUT_ADDR` and `DC_QR_FPR_MISMATCH` constants from CFFI
|
|
||||||
- removed FprWithoutAddr and FprMismatch variants from JSON-RPC QrObject returned by `check_qr`
|
|
||||||
|
|
||||||
### Features / Changes
|
|
||||||
|
|
||||||
- Don't show "member added" messages in channels ([#8730](https://github.com/chatmail/core/pull/8730)).
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- Don't notify about a reaction sent by a blocked contact ([#8729](https://github.com/chatmail/core/pull/8729)).
|
|
||||||
- keep a message read status if a failure arrives later.
|
|
||||||
- prefer login errors over connection errors in the log message ([#8727](https://github.com/chatmail/core/pull/8727)).
|
|
||||||
- blocked or email contacts contacts are never recently seen or old ([#8733](https://github.com/chatmail/core/pull/8733)).
|
|
||||||
|
|
||||||
### Refactor
|
|
||||||
|
|
||||||
- Remove code that moves messages between IMAP folders.
|
|
||||||
|
|
||||||
## [2.61.0] - 2026-09-21
|
|
||||||
|
|
||||||
### API-Changes
|
|
||||||
|
|
||||||
- add `init_transports()` for multi-relay onboarding.
|
|
||||||
- add JSON-RPC API `is_sending_finished()`.
|
|
||||||
- [**breaking**] remove verification methods from the FFI and JSON-RPC APIs.
|
|
||||||
- `dc_contact_is_verified()` and `dc_contact_get_verifier_id()` are removed.
|
|
||||||
- the JSON-RPC Contact object loses the `isVerified` and `verifierId` fields. A bot reading `snapshot.is_verified` now gets an `AttributeError` at runtime.
|
|
||||||
- the Python bindings lose `Contact.is_verified()` and `Contact.get_verifier()`.
|
|
||||||
- `DC_STR_CONTACT_VERIFIED` (35) is removed, so UIs should stop registering a translation for it. A stock id core does not know is logged and otherwise ignored, so an un-updated client keeps working.
|
|
||||||
- [**breaking**] remove default value for "addr" config.
|
|
||||||
- [**breaking**] remove `addr` field from Account objects in JSON-RPC APIs.
|
|
||||||
- `list_transports()` should be used instead.
|
|
||||||
- [**breaking**] remove `is_chatmail` and the XCHATMAIL capability.
|
|
||||||
- `is_chatmail` is no longer a known config key.
|
|
||||||
- [**breaking**] remove `Contact.get_name_n_addr()` and related APIs.
|
|
||||||
- `dc_contact_get_name_n_addr()` CFFI is removed
|
|
||||||
- JSON-RPC contact objects don't have nameAndAddr field anymore
|
|
||||||
- [**breaking**] replace `was_seen_recently` by `freshness` in contact object.
|
|
||||||
- use contact's `freshness` instead of `seen_recently`
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- always emit `AccountsBackgroundFetchDone`.
|
|
||||||
- make `background_fetch` not wait on or trigger SMTP connections.
|
|
||||||
- do not send a sync message when changing `configured_addr`.
|
|
||||||
- emit `SmtpMessageSent` event after deleting the message from SMTP queue.
|
|
||||||
- don't use extra STUN nine server for fallback.
|
|
||||||
- use `max_smtp_rcpt_to` chunking for the actual transport we are sending from.
|
|
||||||
- use correct `From` address when sending MDNs.
|
|
||||||
- use correct address for Bcc-self in unencrypted mails.
|
|
||||||
- don't emit configure progress events during background relay additions.
|
|
||||||
|
|
||||||
### Features / Changes
|
|
||||||
|
|
||||||
- better quality of image recoding ([#8682](https://github.com/chatmail/core/pull/8682)).
|
|
||||||
- perform background fetch from all transports.
|
|
||||||
- queue messages for SMTP before encryption.
|
|
||||||
- [**breaking**] stop tracking contact verification.
|
|
||||||
- the statistics JSON sent to the self-reporting-bot on Android changes: Contacts have `encrypted` instead of `verified` and lose `transitive_chain` properties and message stats have `encrypted` instead of `verified` and `unverified_encrypted`, and securejoin invites lose `already_verified`. The collecting bot stores incoming reports verbatim but analysis will have to make sense of older and newer reports.
|
|
||||||
- remove last usage of XDELTAPUSH capability.
|
|
||||||
- base server-side message deletion on `force_encryption`.
|
|
||||||
- do not restart I/O when setting `configured_addr`.
|
|
||||||
- do not use ConfiguredAddr when connecting to SMTP.
|
|
||||||
- mark autorelays for relay operators ([#8701](https://github.com/chatmail/core/pull/8701)).
|
|
||||||
- try fasted relays to attempt first configure on.
|
|
||||||
- do not mark message as failed for which we got a read receipt before.
|
|
||||||
- a single NDN does not mark a group message as failed.
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- update `sys.msgsize_max_recommended` documentation.
|
|
||||||
- add hint about how to reset an invitation ([#8160](https://github.com/chatmail/core/pull/8160)).
|
|
||||||
- mention `get_app_version()` API in the changelog for 2.59.0.
|
|
||||||
- remove `protect_autocrypt` setting.
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- cleanup "primary" wording in comments.
|
|
||||||
- update rustls to 0.23.45.
|
|
||||||
- fix nightly "cargo" warnings.
|
|
||||||
- fix some types in `deltachat_rpc_client`.
|
|
||||||
|
|
||||||
### Refactor
|
|
||||||
|
|
||||||
- add `Encryption.is_encrypted()`.
|
|
||||||
- separate QueuedEncryption.
|
|
||||||
- sql: disable double-quoted string literals.
|
|
||||||
- add smtp::queue module.
|
|
||||||
- substitute configure progress macro with simple function call.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
- cleanup `get_smtp_rows_for_msg()`.
|
|
||||||
- cross-core securejoin invites for every chat type.
|
|
||||||
- explicitly empty url in appversion updates ([#8702](https://github.com/chatmail/core/pull/8702)).
|
|
||||||
- do not talk about "inconsistent key state" in `test_securejoin_after_contact_resetup`.
|
|
||||||
- rename `test_aeap_transition_{0,1}` to `test_aeap_transition_{single,group}`.
|
|
||||||
- do not fetch all messages when `direct_imap` is created.
|
|
||||||
- `direct_imap`: always pass `mark_seen=False` to fetch().
|
|
||||||
- add pseudo transport explicitly rather than by setting ConfiguredAddr.
|
|
||||||
|
|
||||||
## [2.60.0] - 2026-09-11
|
|
||||||
|
|
||||||
### API-Changes
|
|
||||||
|
|
||||||
- [**breaking**] remove a relay immediately instead of unpublishing it.
|
|
||||||
- `set_transport_unpublished()` is removed: UIs call `delete_transport()` when the user removes a relay.
|
|
||||||
- `list_transports_ex()` and the `TransportListEntry` type are removed: use `list_transports()`.
|
|
||||||
- `delete_transport()` no longer refuses to remove the primary transport: it refuses only to remove the last one and re-elects the sending transport as needed.
|
|
||||||
- `TransportsModified` is now also emitted on the device modifying the transports, not only on devices applying the synced change.
|
|
||||||
- [**breaking**] do not load webxdc icon if it has too large dimensions.
|
|
||||||
- `get_webxdc_blob()` may fail to load `icon.png` or `icon.jpg` if image dimensions are too large.
|
|
||||||
Fixing the issue discovered by https://github.com/Sergei768
|
|
||||||
- Generate JSON-RPC headers at build time ([#8350](https://github.com/chatmail/core/pull/8350)).
|
|
||||||
- Generate Qt JSON-RPC bindings ([#8330](https://github.com/chatmail/core/pull/8330)).
|
|
||||||
|
|
||||||
### Features / Changes
|
|
||||||
|
|
||||||
- Introduce keyupdate messages informing contacts about relay changes.
|
|
||||||
- Remove `Final-Recipient` from MDNs (and keyupdates).
|
|
||||||
- Carry all published relay addresses in securejoin links ([#8591](https://github.com/chatmail/core/pull/8591)).
|
|
||||||
- Use display name for contacts in encryption info ([#8609](https://github.com/chatmail/core/pull/8609)).
|
|
||||||
- Do not create device messages for IMAP authentication errors.
|
|
||||||
- Delete avatars referred to by parameters of special contacts.
|
|
||||||
- Import `Autocrypt-Gossip` keys without checking the addresses.
|
|
||||||
- Ignore `Chat-Disposition-Notification-To` value.
|
|
||||||
- Increase `sys.msgsize_max_recommended` to match chatmail relay message size limit.
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- Do not try to load profile image from param for self.
|
|
||||||
- Send legacy securejoin key requests as `multipart/mixed` so they are not rejected by chatmail relays.
|
|
||||||
- rpc: avoid hang when requests race a dying rpc-server.
|
|
||||||
- Take `timestamp_rcvd` into account in `estimate_deletion_cnt`.
|
|
||||||
- Reliably complete configuration with progress=1000 or progress=0.
|
|
||||||
- Make `create_send_msg_jobs` actually return row IDs.
|
|
||||||
- Don't notify of missed call from blocked user.
|
|
||||||
- Trash MDNs that reference no message early.
|
|
||||||
- Return no relay address for key-contacts without an address.
|
|
||||||
- Do not emit events in `set_profile_image()` if contact avatar is unchanged.
|
|
||||||
- Remove `Original-Recipient` field from MDNs.
|
|
||||||
- ffi: support custom allocators in event string getters.
|
|
||||||
- Start checking column documentation in CI and add comment for `transports.add_timestamp`.
|
|
||||||
- Sanitize `version_string` we got from the wire ([#8582](https://github.com/chatmail/core/pull/8582))
|
|
||||||
- RUSTSEC-2026-0258 ([#8603](https://github.com/chatmail/core/pull/8603)).
|
|
||||||
|
|
||||||
### Build system
|
|
||||||
|
|
||||||
- Use `--locked` in `scripts/clippy.sh`.
|
|
||||||
- Produce correct wheel metadata.
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- Always suggest using `--locked` with "cargo install".
|
|
||||||
- JSON-RPC: clarify when `reactions` is `None`.
|
|
||||||
- Fix async-imap and async-smtp URLs in README.md ([#8637](https://github.com/chatmail/core/pull/8637)).
|
|
||||||
- Update the timeout value in `DC_EVENT_CALL_ENDED` description.
|
|
||||||
|
|
||||||
### Refactor
|
|
||||||
|
|
||||||
- Don't store email address in location KML. ([#8615](https://github.com/chatmail/core/pull/8615)).
|
|
||||||
- Turn `DC_CHAT_ID_*` into `ChatId::*` associated constants.
|
|
||||||
- Turn `DC_MSG_ID_*` into `MsgId::*` associated constants.
|
|
||||||
- Don't include email addresses in export filenames ([#8626](https://github.com/chatmail/core/pull/8626)).
|
|
||||||
- Make `create_send_msg_jobs()` private.
|
|
||||||
- Rename `automatic_relay_management` to autorelay.
|
|
||||||
- Extract shared pieces for non-chat messages.
|
|
||||||
- Remove unused functions from the tools module.
|
|
||||||
- Remove the code to set own avatar in `set_profile_image()`.
|
|
||||||
- Move pgp tests to submodule.
|
|
||||||
- Split `flake.nix` into multiple files.
|
|
||||||
- Use `&[..]` instead of `&Vec<..>`.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
- [**breaking**] rename rpc fixtures to disambiguate from ffi fixtures.
|
|
||||||
- Test `dc_send_msg_sync()`.
|
|
||||||
- Print which error/warning was expected if it does not arrive.
|
|
||||||
|
|
||||||
### CI
|
|
||||||
|
|
||||||
- Update Rust to 1.98.1.
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Add script to show the sizes of futures (async Rust) ([#8536](https://github.com/chatmail/core/pull/8536)).
|
|
||||||
- deps: bump zizmorcore/zizmor-action from 0.6.1 to 0.6.2.
|
|
||||||
- deps: bump swatinem/rust-cache from 2.9.1 to 2.9.2.
|
|
||||||
- deps: bump pypa/gh-action-pypi-publish from 1.14.1 to 1.14.2.
|
|
||||||
- deps: bump taiki-e/install-action from 2.85.1 to 2.86.7.
|
|
||||||
- cargo: bump futures from 0.3.33 to 0.3.34.
|
|
||||||
- cargo: bump thiserror from 2.0.19 to 2.0.20.
|
|
||||||
- cargo: bump syn from 3.0.3 to 3.0.4.
|
|
||||||
- cargo: bump log from 0.4.33 to 0.4.34.
|
|
||||||
- cargo: bump mail-builder from 0.4.4 to 0.5.0.
|
|
||||||
- cargo: bump blake3 from 1.8.5 to 1.8.7.
|
|
||||||
- cargo: bump http-body-util from 0.1.3 to 0.1.5.
|
|
||||||
- cargo: bump uuid from 1.20.0 to 1.25.0.
|
|
||||||
- cargo: bump data-encoding from 2.11.0 to 2.11.1.
|
|
||||||
- bump chacha20 0.10.1 to 0.10.2.
|
|
||||||
|
|
||||||
## [2.59.0] - 2026-08-14
|
|
||||||
|
|
||||||
### API-Changes
|
|
||||||
|
|
||||||
- [**breaking**] Remove deprecated `dc_chat_is_protected()`.
|
|
||||||
- Deprecate `dc_chat_get_info_json()` ([#8580](https://github.com/chatmail/core/pull/8580))
|
|
||||||
- New `get_app_version()` JSON-RPC API to get information about available updates.
|
|
||||||
|
|
||||||
### Features / Changes
|
|
||||||
|
|
||||||
- Add stock strings for being added/removed from group ([#8562](https://github.com/chatmail/core/pull/8562)).
|
|
||||||
- Client version information ([#8557](https://github.com/chatmail/core/pull/8557)).
|
|
||||||
- Remove hidden headers.
|
|
||||||
- Stop creating info messages for old broadcast lists.
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- Filtered reactions are info, not error in device chat.
|
|
||||||
- Send MDNs to self even if MDNs are disabled.
|
|
||||||
- Send HTTP requests in origin not absolute form.
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- json-rpc: improve `reactions_by_contact` doc.
|
|
||||||
- Do not refer to `is_chat_protected()`.
|
|
||||||
- Do not talk about verified chats in securejoin QR-scanning functions.
|
|
||||||
- Add SQL schema documentation.
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Fix nightly clippy warnings.
|
|
||||||
- cargo: bump astral-tokio-tar from 0.6.3 to 0.6.4.
|
|
||||||
- cargo: bump bytes from 1.12.0 to 1.12.1.
|
|
||||||
- FFI: don't swallow but log errors in three places.
|
|
||||||
|
|
||||||
### Refactor
|
|
||||||
|
|
||||||
- Remove `MessengerMessage`.
|
|
||||||
- Stop setting chats.protected column explicitly.
|
|
||||||
- Merge `msg_group_left_local` into `msg_del_member_local` ([#8575](https://github.com/chatmail/core/pull/8575)).
|
|
||||||
- Rename `_ex()` -> `_ext()`.
|
|
||||||
- mimefactory: add Encryption enum.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
- Fix flakyness of iroh tests by sending "forever" so that late swarm-joins still make the test work.
|
|
||||||
- Move iroh tests into separate module.
|
|
||||||
- Provide complete test isolation by not re-using account addresses.
|
|
||||||
- Avoid another source of random failures with `direct_imap` failing to connect on first try.
|
|
||||||
- Remove all cache-related logic in the FFI pytest plugin.
|
|
||||||
- Add a CI-failing check that documented sql schema matches real one.
|
|
||||||
- Abort early if DNS to chatmail domain does not work and nicer pytest startup header.
|
|
||||||
- Load test data through the `data` fixture.
|
|
||||||
- Allow to run the test suite against underscore-domain relays.
|
|
||||||
|
|
||||||
## [2.58.0] - 2026-08-10
|
|
||||||
|
|
||||||
### API-Changes
|
|
||||||
|
|
||||||
- [**breaking**] remove getPushState() and core's internal tracking of it
|
|
||||||
- [**breaking**] remove `dc_chatlist_get_context()`, because it was easy to misuse and likely led to crashes ([#8503](https://github.com/chatmail/core/pull/8503))
|
|
||||||
- instead, store reference-counted Context in `dc_msg_t`, `dc_contact_t` and `dc_chatlist_t`
|
|
||||||
- add "pinned messages" API.
|
|
||||||
|
|
||||||
### Build system
|
|
||||||
|
|
||||||
- update all crates to Rust 2024 edition.
|
|
||||||
|
|
||||||
### CI
|
|
||||||
|
|
||||||
- update github actions monthly instead of weekly.
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- clarify `ChatId::do_set_draft()` docs.
|
|
||||||
- add missing slash to ConnectionSecurity::Starttls doc comment.
|
|
||||||
|
|
||||||
### Features / Changes
|
|
||||||
|
|
||||||
- send Autocrypt pgp key in MDNs occassionally and when relaylist changes.
|
|
||||||
- reduce unncessary gossipping of keys in group chats.
|
|
||||||
- stop requiring XDELTAPUSH capability for push notifications.
|
|
||||||
- prepare basic multi-relay onboarding ([#8444](https://github.com/chatmail/core/pull/8444))
|
|
||||||
- collect ICE servers from all relays.
|
|
||||||
- send messages to 5 relays instead of the newest 3 ones.
|
|
||||||
- allow to send reactions in broadcast channels ([#8450](https://github.com/chatmail/core/pull/8450)).
|
|
||||||
- allow only default reactions in channels broadcast ([#8545](https://github.com/chatmail/core/pull/8545)).
|
|
||||||
- resend pinned state in broadcast channels ([#8549](https://github.com/chatmail/core/pull/8549)).
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- **The primary transport is not synchronized between devices anymore.**
|
|
||||||
- Don't warn about correct EXIF orientation values. ([#8483](https://github.com/chatmail/core/pull/8483)).
|
|
||||||
- deltachat-rpc-client: don't depend on execnet for importing pytest plugin, remove deprecated "py" usage.
|
|
||||||
- send MDNs to all authentic relays of a contact, not just whatever `get_addr()` returns..
|
|
||||||
- mark `as_path()` function unsafe.
|
|
||||||
- python: create event emitter when EventThread is initialized.
|
|
||||||
- Don't download pre-message again if it is known already ([#8488](https://github.com/chatmail/core/pull/8488)).
|
|
||||||
- recognize self addresses in various places (instead of just the "primary").
|
|
||||||
- fix multi relay connectivity view ([#8550](https://github.com/chatmail/core/pull/8550)).
|
|
||||||
- ensure same-second primary transport change propagates correctly.
|
|
||||||
- invalidate `configured_addr` cache before sending transport sync message.
|
|
||||||
- prevent transport de-synchronization because of early fetch cancellation.
|
|
||||||
- improve connectivity HTML if quota info has an error.
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- bump version to 2.58.0-dev.
|
|
||||||
- deps: bump actions/setup-python from 6 to 6.3.0.
|
|
||||||
- deps: bump zizmorcore/zizmor-action from 0.5.7 to 0.6.0.
|
|
||||||
- cargo: bump futures from 0.3.32 to 0.3.33.
|
|
||||||
- cargo: bump tokio from 1.52.3 to 1.53.0.
|
|
||||||
- cargo: bump regex from 1.12.4 to 1.13.1.
|
|
||||||
- disable "large futures" lint again.
|
|
||||||
- cargo: bump tokio-util from 0.7.18 to 0.7.19.
|
|
||||||
- deps: bump zizmorcore/zizmor-action from 0.6.0 to 0.6.1.
|
|
||||||
- deps: bump taiki-e/install-action from 2.83.4 to 2.85.1.
|
|
||||||
- cargo: bump `serde_json` from 1.0.150 to 1.0.151.
|
|
||||||
- deps: bump pypa/gh-action-pypi-publish from 1.14.0 to 1.14.1.
|
|
||||||
- deps: bump actions/setup-python from 6.3.0 to 7.0.0.
|
|
||||||
- cargo: introduce syn 3 dependency.
|
|
||||||
- cargo: bump anyhow from 1.0.103 to 1.0.104.
|
|
||||||
- cargo: bump serde from 1.0.228 to 1.0.229.
|
|
||||||
- cargo: bump thiserror from 2.0.18 to 2.0.19.
|
|
||||||
- cargo: bump libc from 0.2.186 to 0.2.189.
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
|
|
||||||
- Box::pin iroh::endpoint::Builder::bind in order to reduce memory usage.
|
|
||||||
|
|
||||||
### Refactor
|
|
||||||
|
|
||||||
- use the new regex! macro.
|
|
||||||
- Remove FolderMeaning and `target_folder` ([#8456](https://github.com/chatmail/core/pull/8456)).
|
|
||||||
- Unify naming of direct/single/1:1/normal chats ([#8442](https://github.com/chatmail/core/pull/8442)).
|
|
||||||
- un-nest `prepare_msg_blob`.
|
|
||||||
- do not clean `imap_send` table on transport change.
|
|
||||||
- mark enabled ephemeral timer duration as NonZero.
|
|
||||||
- reduce the scope of unsafe in `dc_context_unref()`.
|
|
||||||
- mimefactory: separate rendering of message payload and sendable message.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
- fix flaky `test_markseen_message_and_mdn` test.
|
|
||||||
- fix flaky `test_no_markseen_in_team_profile` ([#8500](https://github.com/chatmail/core/pull/8500)).
|
|
||||||
- Add `test_bcc_self`.
|
|
||||||
- Add test for unencrypted headers ([#8538](https://github.com/chatmail/core/pull/8538)).
|
|
||||||
- Assert log warnings and errors ([#8457](https://github.com/chatmail/core/pull/8457)).
|
|
||||||
|
|
||||||
## [2.57.0] - 2026-07-25
|
|
||||||
|
|
||||||
### API-Changes
|
|
||||||
|
|
||||||
- [**breaking**] remove heartbeat push notifications.
|
|
||||||
- [**breaking**] remove provider-db handling and provider lookup APIs.
|
|
||||||
- provider lookup APIs were removed from CFFI and JSON-RPC.
|
|
||||||
|
|
||||||
also removes offline provider database code and generated provider data,
|
|
||||||
provider-specific fields in configure/transport paths, and REPL providerinfo.
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- remove oauth2 from standards.
|
|
||||||
|
|
||||||
### Features / Changes
|
|
||||||
|
|
||||||
- accept messages from key contacts with forged From address.
|
|
||||||
- enable TLS certificate compression.
|
|
||||||
- read SMTP recipient limit from relay IMAP metadata.
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- fixup CI failures.
|
|
||||||
- never merge outer To headers if standard header protection is used.
|
|
||||||
- Re-add oauth2 to serialized structs ([#8464](https://github.com/chatmail/core/pull/8464)).
|
|
||||||
- migrate transports configured on 2.56 to also have a oauth:false flag.
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- bump version to 2.57.0-dev.
|
|
||||||
- deps: bump actions/setup-node from 6 to 7.
|
|
||||||
- deps: bump cachix/install-nix-action from 31.10.6 to 31.11.0.
|
|
||||||
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.20 to 2.1.1.
|
|
||||||
- deps: bump taiki-e/install-action from 2.82.10 to 2.83.4.
|
|
||||||
- cargo: bump quinn-proto from 0.11.14 to 0.11.16.
|
|
||||||
|
|
||||||
## [2.56.0] - 2026-07-21
|
|
||||||
|
|
||||||
### API-Changes
|
|
||||||
|
|
||||||
- [**breaking**] remove all oauth support and drop DC_LP_AUTH flags.
|
|
||||||
- removed oauth2 module, dc_get_oauth2_url FFI function, DC_LP_AUTH flags and configured/serverflags, and the oauth2 parameter/field from SMTP/IMAP clients, JSON-RPC interfaces, and CLI tools.
|
|
||||||
|
|
||||||
also contains regenerated provider data after dropping oauth in the update script.
|
|
||||||
|
|
||||||
### Features / Changes
|
|
||||||
|
|
||||||
- do not set backup_time in exported databases.
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- revert 207c2e6e4c1bec43204c3b8a46fcbbff67d54b3f because some users reported problems with it.
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- bump version to 2.56.0-dev.
|
|
||||||
|
|
||||||
## [2.55.0] - 2026-07-20
|
|
||||||
|
|
||||||
Minor release to fix CI because releasing 2.54.0 failed.
|
|
||||||
|
|
||||||
### CI
|
|
||||||
|
|
||||||
- Update Node version to 24.
|
|
||||||
|
|
||||||
## [2.54.0] - 2026-07-20
|
|
||||||
|
|
||||||
### API-Changes
|
|
||||||
|
|
||||||
- [**breaking**] Deprecate `is_chatmail`.
|
|
||||||
- UIs should not behave differently for chatmail relays than for classical email servers; most usages of `is_chatmail` can be replaced by `force_encryption`.
|
|
||||||
- [**breaking**] `delete_transport()` must not be used by UIs anymore. Instead, `set_transport_unpublished()` must be called when a user clicks on "Remove".
|
|
||||||
- [**breaking**] `list_transports()` doesn't return unpublished relays anymore.
|
|
||||||
- UIs should use `list_transports()` rather than `list_transports_ex()`, because unpublished transports count as removed from the user point of view, and should not be shown in the relay list anymore.
|
|
||||||
- deltachat-rpc-client: add `Account.set_transport_unpublished()`.
|
|
||||||
- Add `MsgReadCountChanged` event.
|
|
||||||
|
|
||||||
### Features / Changes
|
|
||||||
|
|
||||||
- Implement support for populating and maintaining a list of default relays ([#8341](https://github.com/chatmail/core/pull/8341)).
|
|
||||||
- Remove hidden relays automatically ([#8402](https://github.com/chatmail/core/pull/8402)).
|
|
||||||
- Automatically remove oldest unpublished relay in order to make space when the user wants to add more; don't allow more than 5 relays overall ([#8428](https://github.com/chatmail/core/pull/8428)).
|
|
||||||
- Add silent group changes messages as InNoticed, not InSeen.
|
|
||||||
- Remove `?emailaddress` argument from autoconfig URL that is not using a dedicated domain.
|
|
||||||
- Remove `imap::Session::sync_seen_flags()` ([#7742](https://github.com/chatmail/core/pull/7742)).
|
|
||||||
- Use CAPABILITY response code if IMAP LOGIN command returns it.
|
|
||||||
- Increase max idle timeout for iroh backup receiver to 60 seconds.
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- Request MDNs for resent channel messages.
|
|
||||||
- Make pre-messages w/o text want MDNs ([#8004](https://github.com/chatmail/core/pull/8004)).
|
|
||||||
- Make truncated edited messages have HTML for receivers ([#8249](https://github.com/chatmail/core/pull/8249)).
|
|
||||||
- Un-escape message footer marks in full messages (`get_html`) ([#8427](https://github.com/chatmail/core/pull/8427)).
|
|
||||||
- Hide synced chat if we only know its visibility ([#8343](https://github.com/chatmail/core/pull/8343)).
|
|
||||||
- Tombstone MDN before sending it ([#8252](https://github.com/chatmail/core/pull/8252)).
|
|
||||||
- Recreate `imap_markseen` with `PRIMARY KEY` constraint.
|
|
||||||
- Rerun the full securejoin protocol if the address was outdated ([#8358](https://github.com/chatmail/core/pull/8358)).
|
|
||||||
- Return early from `receive_imf` to not tombstone Iroh-Node-Addr message if webxdc instance isn't found ([#8372](https://github.com/chatmail/core/pull/8372)).
|
|
||||||
- Replace `last_added_location_id` with `last_added_location_timestamp`.
|
|
||||||
- Do not put locations into pre-messages.
|
|
||||||
- RUSTSEC-2026-0204 ([#8403](https://github.com/chatmail/core/pull/8403)).
|
|
||||||
- Ensure public key signatures are not in the past compared to the public key.
|
|
||||||
- Do not bubble up errors in IMAP candidate loop.
|
|
||||||
- Do not log errors if full message is not available on any transport.
|
|
||||||
- Apply reactions that arrived before the message at later time ([#8415](https://github.com/chatmail/core/pull/8415)).
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
|
|
||||||
- Add timestamp to `msgs_index7` and speed up `Chatlist::try_load()` ([#7848](https://github.com/chatmail/core/pull/7848)).
|
|
||||||
|
|
||||||
### CI
|
|
||||||
|
|
||||||
- Update Rust to 1.97.1.
|
|
||||||
- rrsync prepends the restricted upload path, we need to leave it out ([#8405](https://github.com/chatmail/core/pull/8405)).
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- Update STYLE.md: macros should be used only when necessary ([#8410](https://github.com/chatmail/core/pull/8410)).
|
|
||||||
- `create_group_chat_unencrypted()` may lead to chat split on the first device.
|
|
||||||
|
|
||||||
### Refactor
|
|
||||||
|
|
||||||
- Deprecate unused `SkipAutocrypt` param.
|
|
||||||
- Remove commented out `RenderedEmail.envelope`.
|
|
||||||
- Remove the ability to send messages with non-standard header protection.
|
|
||||||
- Make `crate::pgp::symm_encrypt_message` non-async.
|
|
||||||
- Move `ensure_secret_key_exists` into key.rs.
|
|
||||||
- Improve comment ([#8366](https://github.com/chatmail/core/pull/8366)).
|
|
||||||
- Remove `set_modseq()` function.
|
|
||||||
- Remove unnecessary reference in format string.
|
|
||||||
- Label the loop iterating over the candidates.
|
|
||||||
- Remove `GROUP BY c.id` from chatlist queries.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
- securejoin: Check that "vc-{,request-}pubkey" messages don't contain displayname.
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- bump version to 2.54.0-dev.
|
|
||||||
- deps: bump taiki-e/install-action from 2.81.1 to 2.81.8.
|
|
||||||
- deps: bump taiki-e/install-action from 2.81.8 to 2.81.11.
|
|
||||||
- update rPGP from 0.19.0 to 0.20.0.
|
|
||||||
- update astral-tokio-tar from 0.6.2 to 0.6.3.
|
|
||||||
- deps: bump anyhow to 1.0.103.
|
|
||||||
- deps: bump actions/checkout from 6 to 7.
|
|
||||||
- cargo: bump syn from 2.0.117 to 2.0.118.
|
|
||||||
- cargo: bump quote from 1.0.45 to 1.0.46.
|
|
||||||
- cargo: bump bytes from 1.11.1 to 1.12.0.
|
|
||||||
- cargo: bump regex from 1.12.3 to 1.12.4.
|
|
||||||
- cargo: bump log from 0.4.31 to 0.4.33.
|
|
||||||
- cargo: bump hyper from 1.9.0 to 1.10.1.
|
|
||||||
- deps: bump zizmorcore/zizmor-action from 0.5.6 to 0.5.7.
|
|
||||||
- cargo: bump chrono from 0.4.44 to 0.4.45.
|
|
||||||
- update quick-xml to 0.41.0.
|
|
||||||
- cargo: bump brotli from 8.0.2 to 8.0.4.
|
|
||||||
- cargo: bump smallvec from 1.15.1 to 1.15.2.
|
|
||||||
- deps: bump taiki-e/install-action from 2.81.11 to 2.82.6.
|
|
||||||
- update yanked spin@0.9.8 and spin@0.10.0.
|
|
||||||
- deps: bump taiki-e/install-action from 2.82.6 to 2.82.10.
|
|
||||||
- update async-imap to 0.11.3.
|
|
||||||
|
|
||||||
## [2.53.0] - 2026-06-15
|
|
||||||
|
|
||||||
### Features / Changes
|
|
||||||
|
|
||||||
- Make quality of images sent in chats more consistent between images with different aspect ratio.
|
|
||||||
- `MsgId::get_html`: Make only one db query.
|
|
||||||
- Do not log the recipient list for sent messages.
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- Do not trash pre-messages without text but with a webxdc update.
|
|
||||||
- Don't send or process webxdc status updates in pre-messages.
|
|
||||||
- Ignore SecureJoin messages from blocked contacts ([#8295](https://github.com/chatmail/core/pull/8295)).
|
|
||||||
- Do not abort IMAP connection if setting the push token fails.
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- STYLE.md: Require to list columns explicitly in `INSERT` statements.
|
|
||||||
|
|
||||||
### Build system
|
|
||||||
|
|
||||||
- nix: switch to the "master" branch for naersk.
|
|
||||||
- flake.nix: Use hostPlatform.rust.rustcTarget instead of hardcoding it.
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Bump version to 2.52.0-dev.
|
|
||||||
- deps: bump taiki-e/install-action from 2.79.10 to 2.81.1.
|
|
||||||
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.19 to 2.0.20.
|
|
||||||
- Bump version to 2.53.0-dev.
|
|
||||||
|
|
||||||
### Refactor
|
|
||||||
|
|
||||||
- Move the definition of the `target_wh`-variable.
|
|
||||||
- Remove timesmearing.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
- Print multiline chat descriptions with debug formatter.
|
|
||||||
- `exec_securejoin_qr_multi_device()`: Make inviter devices receive each other messages.
|
|
||||||
- Fixup the tests after removing timesmearing.
|
|
||||||
- Remove timeout from `pop_sent_msg_ex()`.
|
|
||||||
|
|
||||||
## [2.52.0] - 2026-06-09
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- Update the channel title after joining if the QR code included a wrong title ([#8260](https://github.com/chatmail/core/pull/8260)).
|
|
||||||
- Don't send removal message to contact that hasn't been a chat member ([#8298](https://github.com/chatmail/core/pull/8298)).
|
|
||||||
|
|
||||||
### Features / Changes
|
|
||||||
|
|
||||||
- Add cryptography-related statistics (`number_of_transports`, `key_version`, `key_algorithm`, `pubkey_size`, `number_of_keys`) ([#8293](https://github.com/chatmail/core/pull/8293), [#8297](https://github.com/chatmail/core/pull/8297)).
|
|
||||||
- Add IMAP folder to `Context::get_info()` ([#8285](https://github.com/chatmail/core/pull/8285)).
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Update preloaded DNS cache.
|
|
||||||
- Use default aws-lc-rs cryptography provider for rustls.
|
|
||||||
- Add exception for unmaintained proc-macro-error2 to deny.toml.
|
|
||||||
- cargo: bump `pin-project` from 1.1.11 to 1.1.13.
|
|
||||||
- cargo: bump `tokio` from 1.52.1 to 1.52.3.
|
|
||||||
- cargo: bump `log` from 0.4.29 to 0.4.30.
|
|
||||||
- cargo: bump `serde_json` from 1.0.149 to 1.0.150.
|
|
||||||
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.18 to 2.0.19.
|
|
||||||
- deps: bump taiki-e/install-action from 2.79.2 to 2.79.10.
|
|
||||||
|
|
||||||
### Build system
|
|
||||||
|
|
||||||
- nix: fix windows cross-compilation by adding pthreads includes.
|
|
||||||
|
|
||||||
### Refactor
|
|
||||||
|
|
||||||
- Remove support for building "source" packages for deltachat-rpc-server.
|
|
||||||
|
|
||||||
## [2.51.0] - 2026-05-29
|
## [2.51.0] - 2026-05-29
|
||||||
|
|
||||||
### Features / Changes
|
### Features / Changes
|
||||||
@@ -8971,15 +8297,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
|||||||
[2.49.0]: https://github.com/chatmail/core/compare/v2.48.0..v2.49.0
|
[2.49.0]: https://github.com/chatmail/core/compare/v2.48.0..v2.49.0
|
||||||
[2.50.0]: https://github.com/chatmail/core/compare/v2.49.0..v2.50.0
|
[2.50.0]: https://github.com/chatmail/core/compare/v2.49.0..v2.50.0
|
||||||
[2.51.0]: https://github.com/chatmail/core/compare/v2.50.0..v2.51.0
|
[2.51.0]: https://github.com/chatmail/core/compare/v2.50.0..v2.51.0
|
||||||
[2.52.0]: https://github.com/chatmail/core/compare/v2.51.0..v2.52.0
|
|
||||||
[2.53.0]: https://github.com/chatmail/core/compare/v2.52.0..v2.53.0
|
|
||||||
[2.54.0]: https://github.com/chatmail/core/compare/v2.53.0..v2.54.0
|
|
||||||
[2.55.0]: https://github.com/chatmail/core/compare/v2.54.0..v2.55.0
|
|
||||||
[2.56.0]: https://github.com/chatmail/core/compare/v2.55.0..v2.56.0
|
|
||||||
[2.57.0]: https://github.com/chatmail/core/compare/v2.56.0..v2.57.0
|
|
||||||
[2.58.0]: https://github.com/chatmail/core/compare/v2.57.0..v2.58.0
|
|
||||||
[2.59.0]: https://github.com/chatmail/core/compare/v2.58.0..v2.59.0
|
|
||||||
[2.60.0]: https://github.com/chatmail/core/compare/v2.59.0..v2.60.0
|
|
||||||
[2.61.0]: https://github.com/chatmail/core/compare/v2.60.0..v2.61.0
|
|
||||||
[2.62.0]: https://github.com/chatmail/core/compare/v2.61.0..v2.62.0
|
|
||||||
[2.63.0]: https://github.com/chatmail/core/compare/v2.62.0..v2.63.0
|
|
||||||
|
|||||||
+28
-42
@@ -2,59 +2,45 @@ cmake_minimum_required(VERSION 3.16)
|
|||||||
project(deltachat LANGUAGES C)
|
project(deltachat LANGUAGES C)
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
option(WITH_JSONRPC_BINDINGS "Generate jsonrpc bindings" OFF)
|
|
||||||
|
|
||||||
find_program(CARGO cargo)
|
find_program(CARGO cargo)
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
set(DYNAMIC_EXT "dylib")
|
set(DYNAMIC_EXT "dylib")
|
||||||
elseif(UNIX)
|
elseif(UNIX)
|
||||||
set(DYNAMIC_EXT "so")
|
set(DYNAMIC_EXT "so")
|
||||||
else()
|
else()
|
||||||
set(DYNAMIC_EXT "dll")
|
set(DYNAMIC_EXT "dll")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(DEFINED ENV{CARGO_BUILD_TARGET})
|
if(DEFINED ENV{CARGO_BUILD_TARGET})
|
||||||
set(CARGO_OUT_DIR "${CMAKE_BINARY_DIR}/target/$ENV{CARGO_BUILD_TARGET}/release")
|
set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
|
||||||
else()
|
else()
|
||||||
set(CARGO_OUT_DIR "${CMAKE_BINARY_DIR}/target/release")
|
set(ARCH_DIR "./")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(WITH_JSONRPC_BINDINGS)
|
add_custom_command(
|
||||||
set(JSONRPC_ARGS --package deltachat-jsonrpc-bindings)
|
OUTPUT
|
||||||
endif()
|
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||||
|
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||||
add_custom_target(
|
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||||
lib_deltachat
|
COMMAND
|
||||||
ALL
|
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||||
COMMAND
|
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
||||||
${CMAKE_COMMAND} -E env
|
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
||||||
CARGO_TARGET_DIR="${CMAKE_BINARY_DIR}/target"
|
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release
|
||||||
PREFIX="${CMAKE_INSTALL_PREFIX}"
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||||
LIBDIR="${CMAKE_INSTALL_FULL_LIBDIR}"
|
|
||||||
INCLUDEDIR="${CMAKE_INSTALL_FULL_INCLUDEDIR}"
|
|
||||||
${CARGO} build --release --package deltachat_ffi ${JSONRPC_ARGS}
|
|
||||||
WORKING_DIRECTORY
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")
|
add_custom_target(
|
||||||
install(FILES "${CARGO_OUT_DIR}/libdeltachat.a" DESTINATION "${CMAKE_INSTALL_LIBDIR}")
|
lib_deltachat
|
||||||
install(FILES "${CARGO_OUT_DIR}/libdeltachat.${DYNAMIC_EXT}" DESTINATION "${CMAKE_INSTALL_LIBDIR}")
|
ALL
|
||||||
install(FILES "${CARGO_OUT_DIR}/pkgconfig/deltachat.pc" DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
|
DEPENDS
|
||||||
|
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||||
|
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||||
|
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||||
|
)
|
||||||
|
|
||||||
if(WITH_JSONRPC_BINDINGS)
|
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||||
install(
|
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||||
FILES
|
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/deltachat-jsonrpc-bindings/qt/generated/types.hpp"
|
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/deltachat-jsonrpc-bindings/qt/generated/client.hpp"
|
|
||||||
DESTINATION
|
|
||||||
"${CMAKE_INSTALL_INCLUDEDIR}/deltachat-jsonrpc/generated"
|
|
||||||
)
|
|
||||||
install(
|
|
||||||
FILES
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/deltachat-jsonrpc-bindings/qt/deltachat-jsonrpc/cffi_client.hpp"
|
|
||||||
DESTINATION
|
|
||||||
"${CMAKE_INSTALL_INCLUDEDIR}/deltachat-jsonrpc"
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|||||||
Generated
+326
-361
File diff suppressed because it is too large
Load Diff
+14
-12
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "2.64.0-dev"
|
version = "2.51.0-dev"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
rust-version = "1.89"
|
rust-version = "1.89"
|
||||||
@@ -36,6 +36,7 @@ strip = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
deltachat_derive = { path = "./deltachat_derive" }
|
deltachat_derive = { path = "./deltachat_derive" }
|
||||||
|
deltachat-time = { path = "./deltachat-time" }
|
||||||
deltachat-contact-tools = { workspace = true }
|
deltachat-contact-tools = { workspace = true }
|
||||||
format-flowed = { path = "./format-flowed" }
|
format-flowed = { path = "./format-flowed" }
|
||||||
ratelimit = { path = "./deltachat-ratelimit" }
|
ratelimit = { path = "./deltachat-ratelimit" }
|
||||||
@@ -43,7 +44,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
|||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
async-broadcast = "0.7.2"
|
async-broadcast = "0.7.2"
|
||||||
async-channel = { workspace = true }
|
async-channel = { workspace = true }
|
||||||
async-imap = { version = "0.12.0", default-features = false, features = ["runtime-tokio", "compress"] }
|
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||||
async-native-tls = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
|
async-native-tls = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
|
||||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||||
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
|
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||||
@@ -69,7 +70,7 @@ iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
|
|||||||
iroh = { version = "0.35", default-features = false }
|
iroh = { version = "0.35", default-features = false }
|
||||||
kamadak-exif = "0.6.1"
|
kamadak-exif = "0.6.1"
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
mail-builder = { version = "1.0.0", default-features = false }
|
mail-builder = { version = "0.4.4", default-features = false }
|
||||||
mailparse = { workspace = true }
|
mailparse = { workspace = true }
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
num_cpus = "1.17"
|
num_cpus = "1.17"
|
||||||
@@ -77,16 +78,18 @@ num-derive = "0.4"
|
|||||||
num-traits = { workspace = true }
|
num-traits = { workspace = true }
|
||||||
parking_lot = "0.12.4"
|
parking_lot = "0.12.4"
|
||||||
percent-encoding = "2.3"
|
percent-encoding = "2.3"
|
||||||
pgp = { version = "0.21.0", features = ["pqc"], default-features = false }
|
pgp = { version = "0.19.0", features = ["draft-pqc"], default-features = false }
|
||||||
pin-project = "1"
|
pin-project = "1"
|
||||||
qrcodegen = "1.7.0"
|
qrcodegen = "1.7.0"
|
||||||
quick-xml = { version = "0.41", features = ["escape-html"] }
|
quick-xml = { version = "0.39", features = ["escape-html"] }
|
||||||
rand-old = { package = "rand", version = "0.8" }
|
rand-old = { package = "rand", version = "0.8" }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||||
sanitize-filename = { workspace = true }
|
sanitize-filename = { workspace = true }
|
||||||
|
sdp = "0.17.1"
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
serde_urlencoded = "0.7.1"
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
sha-1 = "0.10"
|
sha-1 = "0.10"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
@@ -98,12 +101,13 @@ tagger = "4.3.4"
|
|||||||
textwrap = "0.16.2"
|
textwrap = "0.16.2"
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio-io-timeout = "1.2.1"
|
tokio-io-timeout = "1.2.1"
|
||||||
tokio-rustls = { version = "0.26.2", default-features = false, features = ["tls12", "brotli"] }
|
tokio-rustls = { version = "0.26.2", default-features = false, features = ["aws-lc-rs", "tls12"] }
|
||||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||||
astral-tokio-tar = { version = "0.7.0", default-features = false }
|
astral-tokio-tar = { version = "0.6.2", default-features = false }
|
||||||
tokio-util = { workspace = true }
|
tokio-util = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||||
toml = "0.9"
|
toml = "0.9"
|
||||||
|
tracing = "0.1.41"
|
||||||
url = "2"
|
url = "2"
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
@@ -112,7 +116,6 @@ webpki-roots = "0.26.8"
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||||
criterion = { version = "0.8.1", features = ["async_tokio"] }
|
criterion = { version = "0.8.1", features = ["async_tokio"] }
|
||||||
deltachat-time = { path = "./deltachat-time" }
|
|
||||||
futures-lite = { workspace = true }
|
futures-lite = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
@@ -127,7 +130,6 @@ members = [
|
|||||||
"deltachat-ffi",
|
"deltachat-ffi",
|
||||||
"deltachat_derive",
|
"deltachat_derive",
|
||||||
"deltachat-jsonrpc",
|
"deltachat-jsonrpc",
|
||||||
"deltachat-jsonrpc-bindings",
|
|
||||||
"deltachat-rpc-server",
|
"deltachat-rpc-server",
|
||||||
"deltachat-ratelimit",
|
"deltachat-ratelimit",
|
||||||
"deltachat-repl",
|
"deltachat-repl",
|
||||||
@@ -178,7 +180,7 @@ harness = false
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
async-channel = "2.5.0"
|
async-channel = "2.5.0"
|
||||||
base64 = { version = "0.23", default-features = false, features = ["std"] }
|
base64 = "0.22"
|
||||||
chrono = { version = "0.4.44", default-features = false }
|
chrono = { version = "0.4.44", default-features = false }
|
||||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||||
@@ -187,7 +189,7 @@ futures = "0.3.32"
|
|||||||
futures-lite = "2.6.1"
|
futures-lite = "2.6.1"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mailparse = "0.17.0"
|
mailparse = "0.16.1"
|
||||||
nu-ansi-term = "0.50"
|
nu-ansi-term = "0.50"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
@@ -201,7 +203,7 @@ thiserror = "2"
|
|||||||
tokio = "1"
|
tokio = "1"
|
||||||
tokio-util = "0.7.18"
|
tokio-util = "0.7.18"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
yerpc = "0.7"
|
yerpc = "0.6.4"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ The following protocols are handled without requiring API users to know much abo
|
|||||||
|
|
||||||
- secure TLS setup with DNS caching and shadowsocks/proxy support
|
- secure TLS setup with DNS caching and shadowsocks/proxy support
|
||||||
|
|
||||||
- robust [SMTP](https://github.com/chatmail/async-smtp)
|
- robust [SMTP](https://github.com/chatmail/async-imap)
|
||||||
and [IMAP](https://github.com/chatmail/async-imap) handling
|
and [IMAP](https://github.com/chatmail/async-smtp) handling
|
||||||
|
|
||||||
- safe and interoperable [MIME parsing](https://github.com/staktrace/mailparse)
|
- safe and interoperable [MIME parsing](https://github.com/staktrace/mailparse)
|
||||||
and [MIME building](https://github.com/stalwartlabs/mail-builder).
|
and [MIME building](https://github.com/stalwartlabs/mail-builder).
|
||||||
@@ -167,7 +167,7 @@ $ cargo test -- --ignored
|
|||||||
|
|
||||||
Install [`cargo-bolero`](https://github.com/camshaft/bolero) with
|
Install [`cargo-bolero`](https://github.com/camshaft/bolero) with
|
||||||
```sh
|
```sh
|
||||||
$ cargo install --locked cargo-bolero
|
$ cargo install cargo-bolero
|
||||||
```
|
```
|
||||||
|
|
||||||
Run fuzzing tests with
|
Run fuzzing tests with
|
||||||
|
|||||||
@@ -59,13 +59,6 @@ If column is already declared without `NOT NULL`, use `IFNULL` function to provi
|
|||||||
Use `HAVING COUNT(*) > 0` clause
|
Use `HAVING COUNT(*) > 0` clause
|
||||||
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
|
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
|
||||||
|
|
||||||
List columns explicitly in `INSERT` statements:
|
|
||||||
```
|
|
||||||
INSERT OR IGNORE INTO download (rfc724_mid, msg_id) VALUES (?,0);
|
|
||||||
```
|
|
||||||
Otherwise if a new column with default value is added in a future DB version, an upgraded DB can't
|
|
||||||
be used with the old code, e.g. after transferring a DB from a device running a newer version.
|
|
||||||
|
|
||||||
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
|
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
|
||||||
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
|
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
|
||||||
an older version. Also don't change the column type, consider adding a new column with another name
|
an older version. Also don't change the column type, consider adding a new column with another name
|
||||||
@@ -181,10 +174,3 @@ Calling `into()`, `try_into()` or `parse()`
|
|||||||
creates an indirection,
|
creates an indirection,
|
||||||
which is hard to follow for people who are not familiar with Rust,
|
which is hard to follow for people who are not familiar with Rust,
|
||||||
or who are not using rust-analyzer.
|
or who are not using rust-analyzer.
|
||||||
|
|
||||||
## Use macros only when really needed
|
|
||||||
|
|
||||||
Macros can be hard to read for people unfamiliar with Rust,
|
|
||||||
and can have surprising effects like evaluating arguments multiple times.
|
|
||||||
Therefore, macros should only be used when really needed;
|
|
||||||
using functions is usually better.
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-contact-tools"
|
name = "deltachat-contact-tools"
|
||||||
version = "0.0.0" # No semver-stable versioning
|
version = "0.0.0" # No semver-stable versioning
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
|
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
|
|
||||||
|
|||||||
@@ -29,16 +29,17 @@
|
|||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use regex::regex;
|
use anyhow::Result;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
mod vcard;
|
mod vcard;
|
||||||
pub use vcard::{VcardContact, make_vcard, parse_vcard};
|
pub use vcard::{make_vcard, parse_vcard, VcardContact};
|
||||||
|
|
||||||
/// Valid contact address.
|
/// Valid contact address.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ContactAddress(String);
|
pub struct ContactAddress(String);
|
||||||
|
|
||||||
impl Deref for ContactAddress {
|
impl Deref for ContactAddress {
|
||||||
@@ -87,7 +88,9 @@ impl rusqlite::types::ToSql for ContactAddress {
|
|||||||
/// - Removes special characters from the name, see [`sanitize_name()`]
|
/// - Removes special characters from the name, see [`sanitize_name()`]
|
||||||
/// - Removes the name if it is equal to the address by setting it to ""
|
/// - Removes the name if it is equal to the address by setting it to ""
|
||||||
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||||
let (name, addr) = if let Some(captures) = regex!("(.*)<(.*)>").captures(addr.as_ref()) {
|
static ADDR_WITH_NAME_REGEX: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||||
|
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||||
(
|
(
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
captures.get(1).map_or("", |m| m.as_str())
|
captures.get(1).map_or("", |m| m.as_str())
|
||||||
@@ -104,7 +107,7 @@ pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
|||||||
let mut name = sanitize_name(name);
|
let mut name = sanitize_name(name);
|
||||||
|
|
||||||
// If the 'display name' is just the address, remove it:
|
// If the 'display name' is just the address, remove it:
|
||||||
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)".
|
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
|
||||||
// If the display name is empty, DC will just show the address when it needs a display name.
|
// If the display name is empty, DC will just show the address when it needs a display name.
|
||||||
if name == addr {
|
if name == addr {
|
||||||
name = "".to_string();
|
name = "".to_string();
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use regex::regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use crate::sanitize_name_and_addr;
|
use crate::sanitize_name_and_addr;
|
||||||
|
|
||||||
@@ -208,7 +210,9 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
|
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
|
||||||
let unfolded_lines = regex!("\r?\n[\t ]").replace_all(vcard, "");
|
static NEWLINE_AND_SPACE_OR_TAB: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new("\r?\n[\t ]").unwrap());
|
||||||
|
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
|
||||||
|
|
||||||
let mut lines = unfolded_lines.lines().peekable();
|
let mut lines = unfolded_lines.lines().peekable();
|
||||||
let mut contacts = Vec::new();
|
let mut contacts = Vec::new();
|
||||||
|
|||||||
@@ -220,10 +220,7 @@ END:VCARD
|
|||||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||||
assert_eq!(contacts[0].key, None);
|
assert_eq!(contacts[0].key, None);
|
||||||
assert_eq!(
|
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
|
||||||
contacts[0].profile_image.as_deref().unwrap(),
|
|
||||||
"/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q=="
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,10 +244,7 @@ END:VCARD",
|
|||||||
assert_eq!(contacts.len(), 1);
|
assert_eq!(contacts.len(), 1);
|
||||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||||
assert_eq!(&contacts[0].authname, "Alice Wonderland");
|
assert_eq!(&contacts[0].authname, "Alice Wonderland");
|
||||||
assert_eq!(
|
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||||
contacts[0].key.as_ref().unwrap(),
|
|
||||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
||||||
);
|
|
||||||
assert!(contacts[0].timestamp.is_err());
|
assert!(contacts[0].timestamp.is_err());
|
||||||
assert_eq!(contacts[0].profile_image, None);
|
assert_eq!(contacts[0].profile_image, None);
|
||||||
}
|
}
|
||||||
@@ -278,15 +272,9 @@ END:VCARD",
|
|||||||
assert_eq!(contacts.len(), 1);
|
assert_eq!(contacts.len(), 1);
|
||||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||||
assert_eq!(&contacts[0].authname, "Alice");
|
assert_eq!(&contacts[0].authname, "Alice");
|
||||||
assert_eq!(
|
assert_eq!(contacts[0].key.as_ref().unwrap(), "xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==");
|
||||||
contacts[0].key.as_ref().unwrap(),
|
|
||||||
"xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=="
|
|
||||||
);
|
|
||||||
assert!(contacts[0].timestamp.is_err());
|
assert!(contacts[0].timestamp.is_err());
|
||||||
assert_eq!(
|
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
||||||
contacts[0].profile_image.as_ref().unwrap(),
|
|
||||||
"/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "2.64.0-dev"
|
version = "2.51.0-dev"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
edition = "2024"
|
edition = "2018"
|
||||||
|
readme = "README.md"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
|
|
||||||
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ fn main() {
|
|||||||
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or_else(|_| "".to_string()),
|
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or_else(|_| "".to_string()),
|
||||||
version = env::var("CARGO_PKG_VERSION").unwrap(),
|
version = env::var("CARGO_PKG_VERSION").unwrap(),
|
||||||
libs_priv = libs_priv,
|
libs_priv = libs_priv,
|
||||||
prefix = option_env!("PREFIX").unwrap_or_else(|| "/usr/local"),
|
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
|
||||||
libdir = option_env!("LIBDIR").unwrap_or_else(|| "/usr/local/lib"),
|
libdir = env::var("LIBDIR").unwrap_or_else(|_| "/usr/local/lib".to_string()),
|
||||||
includedir = option_env!("INCLUDEDIR").unwrap_or_else(|| "/usr/local/include"),
|
includedir = env::var("INCLUDEDIR").unwrap_or_else(|_| "/usr/local/include".to_string()),
|
||||||
);
|
);
|
||||||
|
|
||||||
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
||||||
|
|||||||
+441
-238
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
use crate::chat::ChatItem;
|
use crate::chat::ChatItem;
|
||||||
|
use crate::constants::DC_MSG_ID_DAYMARKER;
|
||||||
use crate::contact::ContactId;
|
use crate::contact::ContactId;
|
||||||
use crate::location::Location;
|
use crate::location::Location;
|
||||||
use crate::message::MsgId;
|
use crate::message::MsgId;
|
||||||
@@ -20,7 +21,7 @@ impl dc_array_t {
|
|||||||
Self::ContactIds(array) => array[index].to_u32(),
|
Self::ContactIds(array) => array[index].to_u32(),
|
||||||
Self::Chat(array) => match array[index] {
|
Self::Chat(array) => match array[index] {
|
||||||
ChatItem::Message { msg_id } => msg_id.to_u32(),
|
ChatItem::Message { msg_id } => msg_id.to_u32(),
|
||||||
ChatItem::DayMarker { .. } => MsgId::DAYMARKER.to_u32(),
|
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
||||||
},
|
},
|
||||||
Self::Locations(array) => array[index].location_id,
|
Self::Locations(array) => array[index].location_id,
|
||||||
Self::Uint(array) => array[index],
|
Self::Uint(array) => array[index],
|
||||||
|
|||||||
+1275
-1000
File diff suppressed because it is too large
Load Diff
@@ -47,6 +47,8 @@ impl Lot {
|
|||||||
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||||
Qr::AskJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
Qr::AskJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
||||||
Qr::FprOk { .. } => None,
|
Qr::FprOk { .. } => None,
|
||||||
|
Qr::FprMismatch { .. } => None,
|
||||||
|
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
|
||||||
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
||||||
Qr::Backup2 { .. } => None,
|
Qr::Backup2 { .. } => None,
|
||||||
Qr::BackupTooNew { .. } => None,
|
Qr::BackupTooNew { .. } => None,
|
||||||
@@ -101,6 +103,8 @@ impl Lot {
|
|||||||
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
||||||
Qr::AskJoinBroadcast { .. } => LotState::QrAskJoinBroadcast,
|
Qr::AskJoinBroadcast { .. } => LotState::QrAskJoinBroadcast,
|
||||||
Qr::FprOk { .. } => LotState::QrFprOk,
|
Qr::FprOk { .. } => LotState::QrFprOk,
|
||||||
|
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||||
|
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||||
Qr::Account { .. } => LotState::QrAccount,
|
Qr::Account { .. } => LotState::QrAccount,
|
||||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||||
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
|
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
|
||||||
@@ -128,6 +132,8 @@ impl Lot {
|
|||||||
Qr::AskVerifyGroup { .. } => Default::default(),
|
Qr::AskVerifyGroup { .. } => Default::default(),
|
||||||
Qr::AskJoinBroadcast { .. } => Default::default(),
|
Qr::AskJoinBroadcast { .. } => Default::default(),
|
||||||
Qr::FprOk { contact_id } => contact_id.to_u32(),
|
Qr::FprOk { contact_id } => contact_id.to_u32(),
|
||||||
|
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
||||||
|
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||||
Qr::Account { .. } => Default::default(),
|
Qr::Account { .. } => Default::default(),
|
||||||
Qr::Backup2 { .. } => Default::default(),
|
Qr::Backup2 { .. } => Default::default(),
|
||||||
Qr::BackupTooNew { .. } => Default::default(),
|
Qr::BackupTooNew { .. } => Default::default(),
|
||||||
@@ -175,6 +181,12 @@ pub enum LotState {
|
|||||||
/// id=contact
|
/// id=contact
|
||||||
QrFprOk = 210,
|
QrFprOk = 210,
|
||||||
|
|
||||||
|
/// id=contact
|
||||||
|
QrFprMismatch = 220,
|
||||||
|
|
||||||
|
/// text1=formatted fingerprint
|
||||||
|
QrFprWithoutAddr = 230,
|
||||||
|
|
||||||
/// text1=domain
|
/// text1=domain
|
||||||
QrAccount = 250,
|
QrAccount = 250,
|
||||||
|
|
||||||
|
|||||||
+32
-44
@@ -17,15 +17,13 @@ use std::ptr;
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||||
unsafe {
|
let ret: *mut libc::c_char = if !s.is_null() {
|
||||||
let ret: *mut libc::c_char = if !s.is_null() {
|
libc::strdup(s)
|
||||||
libc::strdup(s)
|
} else {
|
||||||
} else {
|
libc::calloc(1, 1) as *mut libc::c_char
|
||||||
libc::calloc(1, 1) as *mut libc::c_char
|
};
|
||||||
};
|
assert!(!ret.is_null());
|
||||||
assert!(!ret.is_null());
|
ret
|
||||||
ret
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error type for the [OsStrExt] trait
|
/// Error type for the [OsStrExt] trait
|
||||||
@@ -166,40 +164,34 @@ pub(crate) trait Strdup {
|
|||||||
/// This function will panic when the original string contains an
|
/// This function will panic when the original string contains an
|
||||||
/// interior null byte as this can not be represented in raw C
|
/// interior null byte as this can not be represented in raw C
|
||||||
/// strings.
|
/// strings.
|
||||||
fn strdup(&self) -> *mut libc::c_char;
|
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Strdup for str {
|
impl Strdup for str {
|
||||||
fn strdup(&self) -> *mut libc::c_char {
|
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||||
unsafe {
|
let tmp = CString::new_lossy(self);
|
||||||
let tmp = CString::new_lossy(self);
|
dc_strdup(tmp.as_ptr())
|
||||||
dc_strdup(tmp.as_ptr())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Strdup for String {
|
impl Strdup for String {
|
||||||
fn strdup(&self) -> *mut libc::c_char {
|
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||||
let s: &str = self;
|
let s: &str = self;
|
||||||
s.strdup()
|
s.strdup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Strdup for std::path::Path {
|
impl Strdup for std::path::Path {
|
||||||
fn strdup(&self) -> *mut libc::c_char {
|
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||||
unsafe {
|
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
||||||
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
dc_strdup(tmp.as_ptr())
|
||||||
dc_strdup(tmp.as_ptr())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Strdup for [u8] {
|
impl Strdup for [u8] {
|
||||||
fn strdup(&self) -> *mut libc::c_char {
|
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||||
unsafe {
|
let tmp = CString::new_lossy(self);
|
||||||
let tmp = CString::new_lossy(self);
|
dc_strdup(tmp.as_ptr())
|
||||||
dc_strdup(tmp.as_ptr())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,15 +209,15 @@ pub(crate) trait OptStrdup {
|
|||||||
/// Allocate a new raw C `*char` version of this string, or NULL.
|
/// Allocate a new raw C `*char` version of this string, or NULL.
|
||||||
///
|
///
|
||||||
/// See [Strdup::strdup] for details.
|
/// See [Strdup::strdup] for details.
|
||||||
fn strdup(&self) -> *mut libc::c_char;
|
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: AsRef<str>> OptStrdup for Option<T> {
|
impl<T: AsRef<str>> OptStrdup for Option<T> {
|
||||||
fn strdup(&self) -> *mut libc::c_char {
|
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||||
match self {
|
match self {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
let tmp = CString::new_lossy(s.as_ref());
|
let tmp = CString::new_lossy(s.as_ref());
|
||||||
unsafe { dc_strdup(tmp.as_ptr()) }
|
dc_strdup(tmp.as_ptr())
|
||||||
}
|
}
|
||||||
None => ptr::null_mut(),
|
None => ptr::null_mut(),
|
||||||
}
|
}
|
||||||
@@ -263,18 +255,20 @@ pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
|||||||
///
|
///
|
||||||
/// [Path]: std::path::Path
|
/// [Path]: std::path::Path
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
pub(crate) unsafe fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
let c_str = unsafe { std::ffi::CStr::from_ptr(s) }.to_bytes();
|
unsafe {
|
||||||
let os_str = std::ffi::OsStr::from_bytes(c_str);
|
let c_str = std::ffi::CStr::from_ptr(s).to_bytes();
|
||||||
std::path::Path::new(os_str)
|
let os_str = std::ffi::OsStr::from_bytes(c_str);
|
||||||
|
std::path::Path::new(os_str)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// as_path() implementation for windows, documented above.
|
// as_path() implementation for windows, documented above.
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub(crate) unsafe fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||||
unsafe { as_path_unicode(s) }
|
as_path_unicode(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementation for as_path() on Windows.
|
// Implementation for as_path() on Windows.
|
||||||
@@ -282,7 +276,7 @@ pub(crate) unsafe fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path
|
|||||||
// Having this as a separate function means it can be tested on unix
|
// Having this as a separate function means it can be tested on unix
|
||||||
// too.
|
// too.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
unsafe fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||||
|
|
||||||
let cstr = unsafe { CStr::from_ptr(s) };
|
let cstr = unsafe { CStr::from_ptr(s) };
|
||||||
@@ -370,20 +364,14 @@ mod tests {
|
|||||||
fn test_as_path() {
|
fn test_as_path() {
|
||||||
let some_path = CString::new("/some/path").unwrap();
|
let some_path = CString::new("/some/path").unwrap();
|
||||||
let ptr = some_path.as_ptr();
|
let ptr = some_path.as_ptr();
|
||||||
assert_eq!(
|
assert_eq!(as_path(ptr), std::ffi::OsString::from("/some/path"))
|
||||||
unsafe { as_path(ptr) },
|
|
||||||
std::ffi::OsString::from("/some/path")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_as_path_unicode_fn() {
|
fn test_as_path_unicode_fn() {
|
||||||
let some_path = CString::new("/some/path").unwrap();
|
let some_path = CString::new("/some/path").unwrap();
|
||||||
let ptr = some_path.as_ptr();
|
let ptr = some_path.as_ptr();
|
||||||
assert_eq!(
|
assert_eq!(as_path_unicode(ptr), std::ffi::OsString::from("/some/path"));
|
||||||
unsafe { as_path_unicode(ptr) },
|
|
||||||
std::ffi::OsString::from("/some/path")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "deltachat-jsonrpc-bindings"
|
|
||||||
version = "2.64.0-dev"
|
|
||||||
description = "Autogenerate DeltaChat JSON-RPC API bindings at build time"
|
|
||||||
edition = "2024"
|
|
||||||
license = "MPL-2.0"
|
|
||||||
repository = "https://github.com/chatmail/core"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
deltachat-jsonrpc = { workspace = true }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["vendored"]
|
|
||||||
vendored = ["deltachat-jsonrpc/vendored"]
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
use deltachat_jsonrpc::api::{write_qt_bindings, write_ts_bindings};
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
write_ts_bindings(Path::new("typescript/generated"));
|
|
||||||
write_qt_bindings(Path::new("qt/generated"), "deltachat");
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
generated
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "deltachat.h"
|
|
||||||
#include "generated/client.hpp"
|
|
||||||
#include "generated/types.hpp"
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <mutex>
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
namespace deltachat {
|
|
||||||
|
|
||||||
class CffiTransport : public Transport {
|
|
||||||
using CompletionHandler = Transport::CompletionHandler;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit CffiTransport(dc_accounts_t *accounts)
|
|
||||||
: jsonrpc_(dc_jsonrpc_init(accounts)) {
|
|
||||||
if (!jsonrpc_)
|
|
||||||
std::abort();
|
|
||||||
thread_ = std::thread([this] { run(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
~CffiTransport() override {
|
|
||||||
done_ = true;
|
|
||||||
// Unblock dc_jsonrpc_next_response by sending a dummy request
|
|
||||||
if (jsonrpc_)
|
|
||||||
dc_jsonrpc_request(
|
|
||||||
jsonrpc_,
|
|
||||||
"{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"get_system_info\"}");
|
|
||||||
if (thread_.joinable())
|
|
||||||
thread_.join();
|
|
||||||
std::lock_guard lk(mu_);
|
|
||||||
for (auto &[id, cb] : pending_) {
|
|
||||||
cb(Result<QJsonValue>::error(-32060, "Transport destructed"));
|
|
||||||
}
|
|
||||||
pending_.clear();
|
|
||||||
if (jsonrpc_)
|
|
||||||
dc_jsonrpc_unref(jsonrpc_);
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void send(const QString method, const QJsonValue params,
|
|
||||||
CompletionHandler onCompleted) override {
|
|
||||||
uint32_t id = next_id_++;
|
|
||||||
QJsonObject envelope{
|
|
||||||
{"jsonrpc", "2.0"},
|
|
||||||
{"id", static_cast<qint64>(id)},
|
|
||||||
{"method", method},
|
|
||||||
{"params", params},
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard lk(mu_);
|
|
||||||
pending_[id] = std::move(onCompleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray json = QJsonDocument(envelope).toJson(QJsonDocument::Compact);
|
|
||||||
dc_jsonrpc_request(jsonrpc_, json.constData());
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void run() {
|
|
||||||
while (!done_) {
|
|
||||||
char *raw_json = dc_jsonrpc_next_response(jsonrpc_);
|
|
||||||
if (!raw_json) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
QByteArray json{raw_json};
|
|
||||||
dc_str_unref(raw_json);
|
|
||||||
if (done_)
|
|
||||||
break;
|
|
||||||
|
|
||||||
QJsonObject obj = QJsonDocument::fromJson(json).object();
|
|
||||||
|
|
||||||
if (!obj["id"].isDouble()) {
|
|
||||||
qCritical() << "No valid rpc id in" << QString{json};
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
uint32_t id = static_cast<uint32_t>(obj["id"].toInt());
|
|
||||||
|
|
||||||
CompletionHandler cb;
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lk(mu_);
|
|
||||||
if (auto nh = pending_.extract(id)) {
|
|
||||||
cb = std::move(nh.mapped());
|
|
||||||
} else {
|
|
||||||
qCritical() << "Could not map response" << QString{json};
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cb(parseResult(obj));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
dc_jsonrpc_instance_t *jsonrpc_;
|
|
||||||
std::thread thread_;
|
|
||||||
std::mutex mu_;
|
|
||||||
std::atomic<uint32_t> next_id_{1};
|
|
||||||
std::atomic<bool> done_{false};
|
|
||||||
std::unordered_map<uint32_t, CompletionHandler> pending_;
|
|
||||||
};
|
|
||||||
|
|
||||||
class CffiDeltaChat : public RawClient {
|
|
||||||
public:
|
|
||||||
explicit CffiDeltaChat(dc_accounts_t *accounts)
|
|
||||||
: RawClient(std::make_unique<CffiTransport>(accounts)) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace deltachat
|
|
||||||
Q_DECLARE_METATYPE(deltachat::CffiDeltaChat *)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-jsonrpc"
|
name = "deltachat-jsonrpc"
|
||||||
version = "2.64.0-dev"
|
version = "2.51.0-dev"
|
||||||
description = "DeltaChat JSON-RPC API"
|
description = "DeltaChat JSON-RPC API"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
repository = "https://github.com/chatmail/core"
|
repository = "https://github.com/chatmail/core"
|
||||||
|
|
||||||
|
|||||||
+107
-154
@@ -5,30 +5,31 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{collections::HashMap, str::FromStr};
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow, bail, ensure};
|
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||||
use deltachat::EventEmitter;
|
|
||||||
pub use deltachat::accounts::Accounts;
|
pub use deltachat::accounts::Accounts;
|
||||||
use deltachat::blob::BlobObject;
|
use deltachat::blob::BlobObject;
|
||||||
use deltachat::calls::ice_servers;
|
use deltachat::calls::ice_servers;
|
||||||
use deltachat::chat::{
|
use deltachat::chat::{
|
||||||
self, Chat, ChatId, ChatItem, MessageListOptions, add_contact_to_chat, forward_msgs,
|
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
|
||||||
forward_msgs_2ctx, get_chat_media, get_chat_msgs, get_chat_msgs_ext, markfresh_chat,
|
get_chat_msgs_ex, markfresh_chat, marknoticed_all_chats, marknoticed_chat,
|
||||||
marknoticed_all_chats, marknoticed_chat, remove_contact_from_chat,
|
remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||||
};
|
};
|
||||||
use deltachat::chatlist::Chatlist;
|
use deltachat::chatlist::Chatlist;
|
||||||
use deltachat::config::{Config, get_all_ui_config_keys};
|
use deltachat::config::{get_all_ui_config_keys, Config};
|
||||||
use deltachat::contact::{Contact, ContactId, Origin, may_be_valid_addr};
|
use deltachat::constants::DC_MSG_ID_DAYMARKER;
|
||||||
|
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
|
||||||
use deltachat::context::get_info;
|
use deltachat::context::get_info;
|
||||||
use deltachat::ephemeral::Timer;
|
use deltachat::ephemeral::Timer;
|
||||||
use deltachat::imex;
|
use deltachat::imex;
|
||||||
use deltachat::location;
|
use deltachat::location;
|
||||||
use deltachat::message::{
|
use deltachat::message::{
|
||||||
self, Message, MessageState, MsgId, Viewtype, delete_msgs_ext, get_existing_msg_ids,
|
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipt_count, get_msg_read_receipts,
|
||||||
get_msg_read_receipt_count, get_msg_read_receipts, markseen_msgs,
|
markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||||
};
|
};
|
||||||
use deltachat::peer_channels::{
|
use deltachat::peer_channels::{
|
||||||
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
|
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
|
||||||
};
|
};
|
||||||
|
use deltachat::provider::get_provider_info;
|
||||||
use deltachat::qr::{self, Qr};
|
use deltachat::qr::{self, Qr};
|
||||||
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
|
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
|
||||||
use deltachat::reaction::{get_msg_reactions, send_reaction};
|
use deltachat::reaction::{get_msg_reactions, send_reaction};
|
||||||
@@ -36,9 +37,10 @@ use deltachat::securejoin;
|
|||||||
use deltachat::stock_str::StockMessage;
|
use deltachat::stock_str::StockMessage;
|
||||||
use deltachat::storage_usage::{get_blobdir_storage_usage, get_storage_usage};
|
use deltachat::storage_usage::{get_blobdir_storage_usage, get_storage_usage};
|
||||||
use deltachat::webxdc::StatusUpdateSerial;
|
use deltachat::webxdc::StatusUpdateSerial;
|
||||||
|
use deltachat::EventEmitter;
|
||||||
use sanitize_filename::is_sanitized;
|
use sanitize_filename::is_sanitized;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio::sync::{Mutex, RwLock, watch};
|
use tokio::sync::{watch, Mutex, RwLock};
|
||||||
use types::login_param::EnteredLoginParam;
|
use types::login_param::EnteredLoginParam;
|
||||||
use yerpc::rpc;
|
use yerpc::rpc;
|
||||||
|
|
||||||
@@ -52,6 +54,8 @@ use types::contact::{ContactObject, VcardContact};
|
|||||||
use types::events::Event;
|
use types::events::Event;
|
||||||
use types::http::HttpResponse;
|
use types::http::HttpResponse;
|
||||||
use types::message::{MessageData, MessageObject, MessageReadReceipt};
|
use types::message::{MessageData, MessageObject, MessageReadReceipt};
|
||||||
|
use types::notify_state::JsonrpcNotifyState;
|
||||||
|
use types::provider_info::ProviderInfo;
|
||||||
use types::reactions::JsonrpcReactions;
|
use types::reactions::JsonrpcReactions;
|
||||||
use types::webxdc::WebxdcMessageInfo;
|
use types::webxdc::WebxdcMessageInfo;
|
||||||
|
|
||||||
@@ -63,8 +67,8 @@ use self::types::{
|
|||||||
JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
|
JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use crate::api::types::appversions::JsonrpcAppSource;
|
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||||
use crate::api::types::chat_list::{ChatListItemFetchResult, get_chat_list_item_by_id};
|
use crate::api::types::login_param::TransportListEntry;
|
||||||
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
|
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -153,7 +157,7 @@ impl CommandApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rpc(all_positional)]
|
#[rpc(all_positional, ts_outdir = "typescript/generated")]
|
||||||
impl CommandApi {
|
impl CommandApi {
|
||||||
/// Test function.
|
/// Test function.
|
||||||
async fn sleep(&self, delay: f64) {
|
async fn sleep(&self, delay: f64) {
|
||||||
@@ -278,26 +282,9 @@ impl CommandApi {
|
|||||||
|
|
||||||
/// Performs a background fetch for all accounts in parallel with a timeout.
|
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||||
///
|
///
|
||||||
/// For an account with IO stopped, the scheduler is paused
|
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout.
|
||||||
/// and every transport is fetched concurrently on a dedicated connection.
|
|
||||||
/// The account is done as soon as one transport received messages, the others stop.
|
|
||||||
/// Only one batch of messages is fetched per transport this way,
|
|
||||||
/// so a larger backlog is left to the next call or to started IO.
|
|
||||||
///
|
|
||||||
/// For an account with IO running, IMAP IDLE is interrupted on every transport
|
|
||||||
/// and the account is done once every transport is.
|
|
||||||
///
|
|
||||||
/// The call never waits for outgoing messages and never triggers sending them itself.
|
|
||||||
/// Received messages may still queue replies, securejoin handshakes for example,
|
|
||||||
/// which go out only while IO is running.
|
|
||||||
/// Use `is_sending_finished()` to tell whether the outgoing queue is empty.
|
|
||||||
///
|
|
||||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout,
|
|
||||||
/// and immediately if another background fetch is already running.
|
|
||||||
/// Process all events until you get this one and you can safely return to the background
|
/// Process all events until you get this one and you can safely return to the background
|
||||||
/// without forgetting to create a generic notification if no message was fetched.
|
/// without forgetting to create notifications caused by timing race conditions.
|
||||||
/// The event carries no data identifying the call it belongs to,
|
|
||||||
/// so it marks your own call only if no concurrent background fetch is happening.
|
|
||||||
async fn background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
async fn background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||||
let future = {
|
let future = {
|
||||||
let lock = self.accounts.read().await;
|
let lock = self.accounts.read().await;
|
||||||
@@ -308,11 +295,6 @@ impl CommandApi {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops an ongoing `background_fetch()` call, making it return early
|
|
||||||
/// without waiting for the remaining transports or for the timeout.
|
|
||||||
///
|
|
||||||
/// The `AccountsBackgroundFetchDone` event is emitted as usual.
|
|
||||||
/// Does nothing if no background fetch is running.
|
|
||||||
async fn stop_background_fetch(&self) -> Result<()> {
|
async fn stop_background_fetch(&self) -> Result<()> {
|
||||||
self.accounts.read().await.stop_background_fetch();
|
self.accounts.read().await.stop_background_fetch();
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -348,6 +330,12 @@ impl CommandApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the current push notification state.
|
||||||
|
async fn get_push_state(&self, account_id: u32) -> Result<JsonrpcNotifyState> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
Ok(ctx.push_state().await.into())
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the combined filesize of an account in bytes
|
/// Get the combined filesize of an account in bytes
|
||||||
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
|
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
@@ -357,6 +345,21 @@ impl CommandApi {
|
|||||||
Ok(dbfile + total_size)
|
Ok(dbfile + total_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns provider for the given domain.
|
||||||
|
///
|
||||||
|
/// This function looks up domain in offline database.
|
||||||
|
///
|
||||||
|
/// For compatibility, email address can be passed to this function
|
||||||
|
/// instead of the domain.
|
||||||
|
async fn get_provider_info(
|
||||||
|
&self,
|
||||||
|
_account_id: u32,
|
||||||
|
email: String,
|
||||||
|
) -> Result<Option<ProviderInfo>> {
|
||||||
|
let provider_info = get_provider_info(email.split('@').next_back().unwrap_or(""));
|
||||||
|
Ok(ProviderInfo::from_dc_type(provider_info))
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks if the context is already configured.
|
/// Checks if the context is already configured.
|
||||||
async fn is_configured(&self, account_id: u32) -> Result<bool> {
|
async fn is_configured(&self, account_id: u32) -> Result<bool> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
@@ -524,6 +527,7 @@ impl CommandApi {
|
|||||||
/// from a server encoded in a QR code.
|
/// from a server encoded in a QR code.
|
||||||
/// - [Self::list_transports()] to get a list of all configured transports.
|
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||||
/// - [Self::delete_transport()] to remove a transport.
|
/// - [Self::delete_transport()] to remove a transport.
|
||||||
|
/// - [Self::set_transport_unpublished()] to set whether contacts see this transport.
|
||||||
async fn add_or_update_transport(
|
async fn add_or_update_transport(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -546,22 +550,26 @@ impl CommandApi {
|
|||||||
ctx.add_transport_from_qr(&qr).await
|
ctx.add_transport_from_qr(&qr).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds an initial transport on the chatmail relay that answers fastest
|
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||||
/// and lets the profile add further ones in the background.
|
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||||
///
|
/// and [Self::delete_transport()] to delete a transport.
|
||||||
/// A `DCACCOUNT:` or `DCLOGIN:` `qr` code adds a single transport
|
/// Use [Self::list_transports_ex()] to additionally query
|
||||||
/// while securejoin codes add the inviter's relays to the candidates.
|
/// whether the transports are marked as 'unpublished'.
|
||||||
///
|
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
|
||||||
/// Does nothing if the profile already has a transport.
|
|
||||||
async fn init_transports(&self, account_id: u32, qr: Option<String>) -> Result<()> {
|
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
ctx.init_transports(qr.as_deref()).await
|
let res = ctx
|
||||||
|
.list_transports()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| t.param.into())
|
||||||
|
.collect();
|
||||||
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||||
/// and [Self::delete_transport()] to remove a transport.
|
/// and [Self::delete_transport()] to delete a transport.
|
||||||
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
|
async fn list_transports_ex(&self, account_id: u32) -> Result<Vec<TransportListEntry>> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let res = ctx
|
let res = ctx
|
||||||
.list_transports()
|
.list_transports()
|
||||||
@@ -572,17 +580,33 @@ impl CommandApi {
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes a transport.
|
/// Removes the transport with the specified email address
|
||||||
/// UIs should call this function when the user removes a relay.
|
/// (i.e. [EnteredLoginParam::addr]).
|
||||||
///
|
|
||||||
/// The last transport cannot be removed.
|
|
||||||
/// If the removed transport was the one used for sending,
|
|
||||||
/// another one is chosen automatically.
|
|
||||||
async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> {
|
async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
ctx.delete_transport(&addr).await
|
ctx.delete_transport(&addr).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change whether the transport is unpublished.
|
||||||
|
///
|
||||||
|
/// Unpublished transports are not advertised to contacts,
|
||||||
|
/// and self-sent messages are not sent there,
|
||||||
|
/// so that we don't cause extra messages to the corresponding inbox,
|
||||||
|
/// but can still receive messages from contacts who don't know our new transport addresses yet.
|
||||||
|
///
|
||||||
|
/// The default is false, but when the user updates from a version that didn't have this flag,
|
||||||
|
/// existing secondary transports are set to unpublished,
|
||||||
|
/// so that an existing transport address doesn't suddenly get spammed with a lot of messages.
|
||||||
|
async fn set_transport_unpublished(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
addr: String,
|
||||||
|
unpublished: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
ctx.set_transport_unpublished(&addr, unpublished).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Signal an ongoing process to stop.
|
/// Signal an ongoing process to stop.
|
||||||
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
|
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
@@ -835,7 +859,7 @@ impl CommandApi {
|
|||||||
/// - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
|
/// - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
|
||||||
/// and the user may create the chat again.
|
/// and the user may create the chat again.
|
||||||
/// - **Groups are not left** - this would
|
/// - **Groups are not left** - this would
|
||||||
/// be unexpected as (1) deleting a single chat also does not prevent new mails
|
/// be unexpected as (1) deleting a normal chat also does not prevent new mails
|
||||||
/// from arriving, (2) leaving a group requires sending a message to
|
/// from arriving, (2) leaving a group requires sending a message to
|
||||||
/// all group members - especially for groups not used for a longer time, this is
|
/// all group members - especially for groups not used for a longer time, this is
|
||||||
/// really unexpected when deletion results in contacting all members again,
|
/// really unexpected when deletion results in contacting all members again,
|
||||||
@@ -859,8 +883,6 @@ impl CommandApi {
|
|||||||
|
|
||||||
/// Get QR code text that will offer a [SecureJoin](https://securejoin.delta.chat/) invitation.
|
/// Get QR code text that will offer a [SecureJoin](https://securejoin.delta.chat/) invitation.
|
||||||
///
|
///
|
||||||
/// To reset invitations, pass the link to `set_config_from_qr()`.
|
|
||||||
///
|
|
||||||
/// If `chat_id` is a group chat ID, SecureJoin QR code for the group is returned.
|
/// If `chat_id` is a group chat ID, SecureJoin QR code for the group is returned.
|
||||||
/// If `chat_id` is unset, setup contact QR code is returned.
|
/// If `chat_id` is unset, setup contact QR code is returned.
|
||||||
async fn get_chat_securejoin_qr_code(
|
async fn get_chat_securejoin_qr_code(
|
||||||
@@ -874,19 +896,20 @@ impl CommandApi {
|
|||||||
Ok(qr)
|
Ok(qr)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get QR code (text and SVG) that will offer a SecureJoin invitation.
|
/// Get QR code (text and SVG) that will offer a Setup-Contact or Verified-Group invitation.
|
||||||
/// The QR code is compatible to the OPENPGP4FPR format
|
/// The QR code is compatible to the OPENPGP4FPR format
|
||||||
/// so that a basic fingerprint comparison also works e.g. with OpenKeychain.
|
/// so that a basic fingerprint comparison also works e.g. with OpenKeychain.
|
||||||
///
|
///
|
||||||
/// The scanning device will pass the scanned content to `checkQr()` then;
|
/// The scanning device will pass the scanned content to `checkQr()` then;
|
||||||
/// if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
|
/// if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
|
||||||
/// the securejoin protocol can be started using `secure_join()`
|
/// an out-of-band-verification can be joined using `secure_join()`
|
||||||
///
|
///
|
||||||
/// @deprecated as of 2026-03; use create_qr_svg(get_chat_securejoin_qr_code()) instead.
|
/// @deprecated as of 2026-03; use create_qr_svg(get_chat_securejoin_qr_code()) instead.
|
||||||
///
|
///
|
||||||
/// chat_id: If set to a group-chat-id,
|
/// chat_id: If set to a group-chat-id,
|
||||||
/// the SecureJoin QR code for the group is returned.
|
/// the Verified-Group-Invite protocol is offered in the QR code;
|
||||||
/// If not set, the setup contact QR code is returned.
|
/// works for protected groups as well as for normal groups.
|
||||||
|
/// If not set, the Setup-Contact protocol is offered in the QR code.
|
||||||
/// See https://securejoin.delta.chat/ for details about both protocols.
|
/// See https://securejoin.delta.chat/ for details about both protocols.
|
||||||
///
|
///
|
||||||
/// return format: `[code, svg]`
|
/// return format: `[code, svg]`
|
||||||
@@ -902,7 +925,7 @@ impl CommandApi {
|
|||||||
Ok((qr, svg))
|
Ok((qr, svg))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Continue the SecureJoin protocol
|
/// Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||||
/// started on another device with `get_chat_securejoin_qr_code_svg()`.
|
/// started on another device with `get_chat_securejoin_qr_code_svg()`.
|
||||||
/// This function is typically called when `check_qr()` returns
|
/// This function is typically called when `check_qr()` returns
|
||||||
/// type=AskVerifyContact or type=AskVerifyGroup.
|
/// type=AskVerifyContact or type=AskVerifyGroup.
|
||||||
@@ -920,6 +943,7 @@ impl CommandApi {
|
|||||||
/// to `check_qr()`.
|
/// to `check_qr()`.
|
||||||
///
|
///
|
||||||
/// **returns**: The chat ID of the joined chat, the UI may redirect to the this chat.
|
/// **returns**: The chat ID of the joined chat, the UI may redirect to the this chat.
|
||||||
|
/// A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
|
||||||
///
|
///
|
||||||
async fn secure_join(&self, account_id: u32, qr: String) -> Result<u32> {
|
async fn secure_join(&self, account_id: u32, qr: String) -> Result<u32> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
@@ -985,6 +1009,8 @@ impl CommandApi {
|
|||||||
/// If the group is already _promoted_ (any message was sent to the group),
|
/// If the group is already _promoted_ (any message was sent to the group),
|
||||||
/// all group members are informed by a special status message that is sent automatically by this function.
|
/// all group members are informed by a special status message that is sent automatically by this function.
|
||||||
///
|
///
|
||||||
|
/// If the group has group protection enabled, only verified contacts can be added to the group.
|
||||||
|
///
|
||||||
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||||
async fn add_contact_to_chat(
|
async fn add_contact_to_chat(
|
||||||
&self,
|
&self,
|
||||||
@@ -998,7 +1024,7 @@ impl CommandApi {
|
|||||||
|
|
||||||
/// Get the contact IDs belonging to a chat.
|
/// Get the contact IDs belonging to a chat.
|
||||||
///
|
///
|
||||||
/// - for single chats, the function always returns exactly one contact,
|
/// - for normal chats, the function always returns exactly one contact,
|
||||||
/// DC_CONTACT_ID_SELF is returned only for SELF-chats.
|
/// DC_CONTACT_ID_SELF is returned only for SELF-chats.
|
||||||
///
|
///
|
||||||
/// - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
|
/// - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
|
||||||
@@ -1055,9 +1081,7 @@ impl CommandApi {
|
|||||||
/// Create a new unencrypted group chat.
|
/// Create a new unencrypted group chat.
|
||||||
///
|
///
|
||||||
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
|
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
|
||||||
/// address-contacts. NB: Chats with similar names and the same members are merged on other
|
/// address-contacts.
|
||||||
/// devices, but usually users don't create such chats and look up the existing one instead, so
|
|
||||||
/// chat split on the first device is acceptable.
|
|
||||||
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
|
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
chat::create_group_unencrypted(&ctx, &name)
|
chat::create_group_unencrypted(&ctx, &name)
|
||||||
@@ -1326,7 +1350,7 @@ impl CommandApi {
|
|||||||
/// The concrete action depends on the type of the chat and on the users settings
|
/// The concrete action depends on the type of the chat and on the users settings
|
||||||
/// (dc_msgs_presented() may be a better name therefore, but well. :)
|
/// (dc_msgs_presented() may be a better name therefore, but well. :)
|
||||||
///
|
///
|
||||||
/// - For single chats, the IMAP state is updated, MDN is sent
|
/// - For normal chats, the IMAP state is updated, MDN is sent
|
||||||
/// (if set_config()-options `mdns_enabled` is set)
|
/// (if set_config()-options `mdns_enabled` is set)
|
||||||
/// and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
|
/// and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
|
||||||
///
|
///
|
||||||
@@ -1364,7 +1388,7 @@ impl CommandApi {
|
|||||||
///
|
///
|
||||||
/// * chat_id The chat ID of which the messages IDs should be queried.
|
/// * chat_id The chat ID of which the messages IDs should be queried.
|
||||||
/// * _info_only: Deprecated, pass `false` here.
|
/// * _info_only: Deprecated, pass `false` here.
|
||||||
/// * `add_daymarker` - If `true`, add day markers as `MsgId::DAYMARKER` to the result,
|
/// * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
|
||||||
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
|
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
|
||||||
/// corresponding (following) day in the local timezone.
|
/// corresponding (following) day in the local timezone.
|
||||||
async fn get_message_ids(
|
async fn get_message_ids(
|
||||||
@@ -1375,7 +1399,7 @@ impl CommandApi {
|
|||||||
add_daymarker: bool,
|
add_daymarker: bool,
|
||||||
) -> Result<Vec<u32>> {
|
) -> Result<Vec<u32>> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let msg = get_chat_msgs_ext(
|
let msg = get_chat_msgs_ex(
|
||||||
&ctx,
|
&ctx,
|
||||||
ChatId::new(chat_id),
|
ChatId::new(chat_id),
|
||||||
MessageListOptions { add_daymarker },
|
MessageListOptions { add_daymarker },
|
||||||
@@ -1386,7 +1410,7 @@ impl CommandApi {
|
|||||||
.map(|chat_item| -> u32 {
|
.map(|chat_item| -> u32 {
|
||||||
match chat_item {
|
match chat_item {
|
||||||
deltachat::chat::ChatItem::Message { msg_id } => msg_id.to_u32(),
|
deltachat::chat::ChatItem::Message { msg_id } => msg_id.to_u32(),
|
||||||
deltachat::chat::ChatItem::DayMarker { .. } => MsgId::DAYMARKER.to_u32(),
|
deltachat::chat::ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
@@ -1424,7 +1448,7 @@ impl CommandApi {
|
|||||||
add_daymarker: bool,
|
add_daymarker: bool,
|
||||||
) -> Result<Vec<JsonrpcMessageListItem>> {
|
) -> Result<Vec<JsonrpcMessageListItem>> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let msg = get_chat_msgs_ext(
|
let msg = get_chat_msgs_ex(
|
||||||
&ctx,
|
&ctx,
|
||||||
ChatId::new(chat_id),
|
ChatId::new(chat_id),
|
||||||
MessageListOptions { add_daymarker },
|
MessageListOptions { add_daymarker },
|
||||||
@@ -1490,32 +1514,12 @@ impl CommandApi {
|
|||||||
MessageNotificationInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
|
MessageNotificationInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the "pinned" state for a message.
|
|
||||||
async fn set_pinned_message_state(
|
|
||||||
&self,
|
|
||||||
account_id: u32,
|
|
||||||
message_id: u32,
|
|
||||||
pinned_state: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
let ctx = self.get_context(account_id).await?;
|
|
||||||
deltachat::pinned_messages::set_pinned_state(&ctx, MsgId::new(message_id), pinned_state)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns all pinned messages of a chat.
|
|
||||||
async fn get_pinned_messages(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
|
|
||||||
let ctx = self.get_context(account_id).await?;
|
|
||||||
let msg_ids =
|
|
||||||
deltachat::pinned_messages::get_pinned_messages(&ctx, ChatId::new(chat_id)).await?;
|
|
||||||
Ok(msg_ids.into_iter().map(|id| id.to_u32()).collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete messages. The messages are deleted on the current device and
|
/// Delete messages. The messages are deleted on the current device and
|
||||||
/// on the IMAP server.
|
/// on the IMAP server.
|
||||||
async fn delete_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
async fn delete_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
||||||
delete_msgs_ext(&ctx, &msgs, false).await
|
delete_msgs_ex(&ctx, &msgs, false).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete messages. The messages are deleted on the current device,
|
/// Delete messages. The messages are deleted on the current device,
|
||||||
@@ -1523,7 +1527,7 @@ impl CommandApi {
|
|||||||
async fn delete_messages_for_all(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
async fn delete_messages_for_all(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
||||||
delete_msgs_ext(&ctx, &msgs, true).await
|
delete_msgs_ex(&ctx, &msgs, true).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an informational text for a single message. The text is multiline and may
|
/// Get an informational text for a single message. The text is multiline and may
|
||||||
@@ -1806,7 +1810,7 @@ impl CommandApi {
|
|||||||
|
|
||||||
/// Get encryption info for a contact.
|
/// Get encryption info for a contact.
|
||||||
/// Get a multi-line encryption info, containing your fingerprint and the
|
/// Get a multi-line encryption info, containing your fingerprint and the
|
||||||
/// fingerprint of the contact, used e.g. to compare the fingerprints out-of-band.
|
/// fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
|
||||||
async fn get_contact_encryption_info(
|
async fn get_contact_encryption_info(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1888,7 +1892,7 @@ impl CommandApi {
|
|||||||
// chat
|
// chat
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
|
|
||||||
/// Returns the [`ChatId`] for the single chat with `contact_id` if it exists.
|
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
|
||||||
///
|
///
|
||||||
/// If it does not exist, `None` is returned.
|
/// If it does not exist, `None` is returned.
|
||||||
async fn get_chat_id_by_contact_id(
|
async fn get_chat_id_by_contact_id(
|
||||||
@@ -2071,20 +2075,15 @@ impl CommandApi {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Waits until all transports are idle or failed and no background work is left.
|
|
||||||
/// Never returns unless I/O is started. Must ONLY be used by tests.
|
|
||||||
async fn wait_for_all_work_done(&self, account_id: u32) -> Result<()> {
|
|
||||||
let ctx = self.get_context(account_id).await?;
|
|
||||||
ctx.wait_for_all_work_done().await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current connectivity, i.e. whether the device is connected to the IMAP server.
|
/// Get the current connectivity, i.e. whether the device is connected to the IMAP server.
|
||||||
/// One of:
|
/// One of:
|
||||||
/// - DC_CONNECTIVITY_NOT_CONNECTED (1000): Show e.g. the string "Not connected" or a red dot
|
/// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
|
||||||
/// - DC_CONNECTIVITY_CONNECTING (2000): Show e.g. the string "Connecting…" or a yellow dot
|
/// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
|
||||||
/// - DC_CONNECTIVITY_WORKING (3000): Show e.g. the string "Getting new messages" or a spinning wheel
|
/// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
|
||||||
/// - DC_CONNECTIVITY_CONNECTED (4000): Show e.g. the string "Connected" or a green dot
|
/// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
|
||||||
|
///
|
||||||
|
/// We don't use exact values but ranges here so that we can split up
|
||||||
|
/// states into multiple states in the future.
|
||||||
///
|
///
|
||||||
/// Meant as a rough overview that can be shown
|
/// Meant as a rough overview that can be shown
|
||||||
/// e.g. in the title of the main screen.
|
/// e.g. in the title of the main screen.
|
||||||
@@ -2268,9 +2267,6 @@ impl CommandApi {
|
|||||||
/// Get blob encoded as base64 from a webxdc message
|
/// Get blob encoded as base64 from a webxdc message
|
||||||
///
|
///
|
||||||
/// path is the path of the file within webxdc archive
|
/// path is the path of the file within webxdc archive
|
||||||
///
|
|
||||||
/// If the file is `icon.png` or `icon.jpg`,
|
|
||||||
/// loading it may fail if dimensions are unexpectedly large.
|
|
||||||
async fn get_webxdc_blob(
|
async fn get_webxdc_blob(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -2281,7 +2277,7 @@ impl CommandApi {
|
|||||||
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
|
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
|
||||||
let blob = message.get_webxdc_blob(&ctx, &path).await?;
|
let blob = message.get_webxdc_blob(&ctx, &path).await?;
|
||||||
|
|
||||||
use base64::{Engine as _, engine::general_purpose};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
|
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2451,7 +2447,6 @@ impl CommandApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns reactions to the message.
|
/// Returns reactions to the message.
|
||||||
/// `None` when there are no reactions.
|
|
||||||
async fn get_message_reactions(
|
async fn get_message_reactions(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -2783,48 +2778,6 @@ impl CommandApi {
|
|||||||
Err(anyhow!("chat with id {chat_id} doesn't have draft message"))
|
Err(anyhow!("chat with id {chat_id} doesn't have draft message"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get version information of a specific client and source
|
|
||||||
/// across all configured accounts and transports.
|
|
||||||
///
|
|
||||||
/// Returns the source with the highest `version_integer`.
|
|
||||||
/// If no matching version information is available at all, `None` is returned.
|
|
||||||
///
|
|
||||||
/// UIs shall call the function after a reasonable time after app start,
|
|
||||||
/// when most relays have reported the information they have, say 30 seconds.
|
|
||||||
/// After that, once a day.
|
|
||||||
/// (it is accepted if by the simple approach an update message is delayed.
|
|
||||||
/// an event was considered, but that seemed more complex for few benefit:
|
|
||||||
/// as we do not know if "late" relays will report "better" versions,
|
|
||||||
/// also there we would work with timeouts etc.)
|
|
||||||
///
|
|
||||||
/// If the reported `version_integer` is larger than the running app version,
|
|
||||||
/// the UI shall report to the user, that an update is available,
|
|
||||||
/// and, if possible, offer a direct update by the given URL.
|
|
||||||
///
|
|
||||||
/// Security note: consumers need to verify themselves
|
|
||||||
/// that downloaded app files are valid before installing them.
|
|
||||||
async fn get_app_version(
|
|
||||||
&self,
|
|
||||||
client_id: String,
|
|
||||||
source_id: String,
|
|
||||||
) -> Result<Option<JsonrpcAppSource>> {
|
|
||||||
let accounts = self.accounts.read().await;
|
|
||||||
Ok(
|
|
||||||
deltachat::appversions::get_app_version(&accounts, &client_id, &source_id)
|
|
||||||
.await?
|
|
||||||
.map(JsonrpcAppSource::from_core_type),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if all accounts have empty outgoing message queue.
|
|
||||||
///
|
|
||||||
/// This API is intended to be used by UIs
|
|
||||||
/// to request that operating system does not put the application in background
|
|
||||||
/// while there are still outgoing messages that are not sent out.
|
|
||||||
async fn is_sending_finished(&self) -> Result<bool> {
|
|
||||||
self.accounts.read().await.is_sending_finished().await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions (to prevent code duplication)
|
// Helper functions (to prevent code duplication)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub enum Account {
|
|||||||
Configured {
|
Configured {
|
||||||
id: u32,
|
id: u32,
|
||||||
display_name: Option<String>,
|
display_name: Option<String>,
|
||||||
|
addr: Option<String>,
|
||||||
// size: u32,
|
// size: u32,
|
||||||
profile_image: Option<String>,
|
profile_image: Option<String>,
|
||||||
color: String,
|
color: String,
|
||||||
@@ -28,6 +29,7 @@ impl Account {
|
|||||||
pub async fn from_context(ctx: &deltachat::context::Context, id: u32) -> Result<Self> {
|
pub async fn from_context(ctx: &deltachat::context::Context, id: u32) -> Result<Self> {
|
||||||
if ctx.is_configured().await? {
|
if ctx.is_configured().await? {
|
||||||
let display_name = ctx.get_config(Config::Displayname).await?;
|
let display_name = ctx.get_config(Config::Displayname).await?;
|
||||||
|
let addr = ctx.get_config(Config::Addr).await?;
|
||||||
let profile_image = ctx.get_config(Config::Selfavatar).await?;
|
let profile_image = ctx.get_config(Config::Selfavatar).await?;
|
||||||
let color = color_int_to_hex_string(
|
let color = color_int_to_hex_string(
|
||||||
Contact::get_by_id(ctx, ContactId::SELF)
|
Contact::get_by_id(ctx, ContactId::SELF)
|
||||||
@@ -39,6 +41,7 @@ impl Account {
|
|||||||
Ok(Account::Configured {
|
Ok(Account::Configured {
|
||||||
id,
|
id,
|
||||||
display_name,
|
display_name,
|
||||||
|
addr,
|
||||||
profile_image,
|
profile_image,
|
||||||
color,
|
color,
|
||||||
private_tag,
|
private_tag,
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
use deltachat::appversions::AppSource;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use typescript_type_def::TypeDef;
|
|
||||||
|
|
||||||
/// Version information of a single source of a client, eg. "gplay" or "fdroid".
|
|
||||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
|
||||||
#[serde(rename = "AppSource", rename_all = "camelCase")]
|
|
||||||
pub struct JsonrpcAppSource {
|
|
||||||
/// Always increasing version number.
|
|
||||||
pub version_integer: u32,
|
|
||||||
|
|
||||||
/// Version string that should be shown to the user.
|
|
||||||
/// UI must not linkify the string
|
|
||||||
/// as it may be interpreted like a phone number or an IP address.
|
|
||||||
pub version_string: String,
|
|
||||||
|
|
||||||
/// Where to download that version.
|
|
||||||
/// Security note: consumers need to verify themselves
|
|
||||||
/// that downloaded app files are valid before installing them.
|
|
||||||
pub download_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JsonrpcAppSource {
|
|
||||||
pub fn from_core_type(source: AppSource) -> Self {
|
|
||||||
JsonrpcAppSource {
|
|
||||||
version_integer: source.version_integer,
|
|
||||||
version_string: source.version_string,
|
|
||||||
download_url: source.download_url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
|
|
||||||
use deltachat::calls::{CallState, call_state};
|
use deltachat::calls::{call_state, CallState};
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::message::MsgId;
|
use deltachat::message::MsgId;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, bail};
|
use anyhow::{bail, Context as _, Result};
|
||||||
use deltachat::chat::{self, ChatVisibility, get_chat_contacts, get_past_chat_contacts};
|
use deltachat::chat::{self, get_chat_contacts, get_past_chat_contacts, ChatVisibility};
|
||||||
use deltachat::chat::{Chat, ChatId};
|
use deltachat::chat::{Chat, ChatId};
|
||||||
use deltachat::constants::Chattype;
|
use deltachat::constants::Chattype;
|
||||||
use deltachat::contact::{Contact, ContactId};
|
use deltachat::contact::{Contact, ContactId};
|
||||||
@@ -9,8 +9,6 @@ use deltachat::context::Context;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
use crate::api::types::contact::ContactFreshness;
|
|
||||||
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
@@ -71,7 +69,7 @@ pub struct FullChat {
|
|||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
ephemeral_timer: u32,
|
ephemeral_timer: u32,
|
||||||
can_send: bool,
|
can_send: bool,
|
||||||
freshness: ContactFreshness,
|
was_seen_recently: bool,
|
||||||
mailing_list_address: Option<String>,
|
mailing_list_address: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,17 +92,16 @@ impl FullChat {
|
|||||||
|
|
||||||
let can_send = chat.can_send(context).await?;
|
let can_send = chat.can_send(context).await?;
|
||||||
|
|
||||||
let freshness = if chat.get_type() == Chattype::Single {
|
let was_seen_recently = if chat.get_type() == Chattype::Single {
|
||||||
match contact_ids.first() {
|
match contact_ids.first() {
|
||||||
Some(contact) => Contact::get_by_id(context, *contact)
|
Some(contact) => Contact::get_by_id(context, *contact)
|
||||||
.await
|
.await
|
||||||
.context("failed to load contact for get_freshness")?
|
.context("failed to load contact for was_seen_recently")?
|
||||||
.get_freshness()
|
.was_seen_recently(),
|
||||||
.into(),
|
None => false,
|
||||||
None => ContactFreshness::Normal,
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ContactFreshness::Normal
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
let mailing_list_address = chat.get_mailinglist_addr().map(|s| s.to_string());
|
let mailing_list_address = chat.get_mailinglist_addr().map(|s| s.to_string());
|
||||||
@@ -129,7 +126,7 @@ impl FullChat {
|
|||||||
is_muted: chat.is_muted(),
|
is_muted: chat.is_muted(),
|
||||||
ephemeral_timer,
|
ephemeral_timer,
|
||||||
can_send,
|
can_send,
|
||||||
freshness,
|
was_seen_recently,
|
||||||
mailing_list_address,
|
mailing_list_address,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -140,7 +137,7 @@ impl FullChat {
|
|||||||
/// - fresh_message_counter
|
/// - fresh_message_counter
|
||||||
/// - ephemeral_timer
|
/// - ephemeral_timer
|
||||||
/// - self_in_group
|
/// - self_in_group
|
||||||
/// - freshness
|
/// - was_seen_recently
|
||||||
/// - can_send
|
/// - can_send
|
||||||
///
|
///
|
||||||
/// used when you only need the basic metadata of a chat like type, name, profile picture
|
/// used when you only need the basic metadata of a chat like type, name, profile picture
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ use deltachat::chatlist::get_last_message_for_chat;
|
|||||||
use deltachat::constants::*;
|
use deltachat::constants::*;
|
||||||
use deltachat::contact::Contact;
|
use deltachat::contact::Contact;
|
||||||
use deltachat::{
|
use deltachat::{
|
||||||
chat::{ChatVisibility, get_chat_contacts},
|
chat::{get_chat_contacts, ChatVisibility},
|
||||||
chatlist::Chatlist,
|
chatlist::Chatlist,
|
||||||
};
|
};
|
||||||
use num_traits::cast::ToPrimitive;
|
use num_traits::cast::ToPrimitive;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
use crate::api::types::contact::ContactFreshness;
|
|
||||||
|
|
||||||
use super::chat::JsonrpcChatType;
|
use super::chat::JsonrpcChatType;
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
use super::message::MessageViewtype;
|
use super::message::MessageViewtype;
|
||||||
@@ -31,6 +29,8 @@ pub enum ChatListItemFetchResult {
|
|||||||
summary_text1: String,
|
summary_text1: String,
|
||||||
summary_text2: String,
|
summary_text2: String,
|
||||||
summary_status: u32,
|
summary_status: u32,
|
||||||
|
/// showing preview if last chat message is image
|
||||||
|
summary_preview_image: Option<String>,
|
||||||
|
|
||||||
/// True if the chat is encrypted.
|
/// True if the chat is encrypted.
|
||||||
/// This means that all messages in the chat are encrypted,
|
/// This means that all messages in the chat are encrypted,
|
||||||
@@ -68,7 +68,7 @@ pub enum ChatListItemFetchResult {
|
|||||||
is_contact_request: bool,
|
is_contact_request: bool,
|
||||||
/// contact id if this is a dm chat (for view profile entry in context menu)
|
/// contact id if this is a dm chat (for view profile entry in context menu)
|
||||||
dm_chat_contact: Option<u32>,
|
dm_chat_contact: Option<u32>,
|
||||||
freshness: ContactFreshness,
|
was_seen_recently: bool,
|
||||||
last_message_type: Option<MessageViewtype>,
|
last_message_type: Option<MessageViewtype>,
|
||||||
last_message_id: Option<u32>,
|
last_message_id: Option<u32>,
|
||||||
},
|
},
|
||||||
@@ -101,6 +101,8 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
|
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
|
||||||
let summary_text2 = summary.text.to_owned();
|
let summary_text2 = summary.text.to_owned();
|
||||||
|
|
||||||
|
let summary_preview_image = summary.thumbnail_path;
|
||||||
|
|
||||||
let visibility = chat.get_visibility();
|
let visibility = chat.get_visibility();
|
||||||
|
|
||||||
let avatar_path = chat
|
let avatar_path = chat
|
||||||
@@ -125,20 +127,22 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
None => (None, None),
|
None => (None, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (dm_chat_contact, freshness) = if chat.get_type() == Chattype::Single {
|
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
|
||||||
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
||||||
let contact = chat_contacts.first();
|
let contact = chat_contacts.first();
|
||||||
let freshness = match contact {
|
let was_seen_recently = match contact {
|
||||||
Some(contact) => Contact::get_by_id(ctx, *contact)
|
Some(contact) => Contact::get_by_id(ctx, *contact)
|
||||||
.await
|
.await
|
||||||
.context("contact")?
|
.context("contact")?
|
||||||
.get_freshness()
|
.was_seen_recently(),
|
||||||
.into(),
|
None => false,
|
||||||
None => ContactFreshness::Normal,
|
|
||||||
};
|
};
|
||||||
(contact.map(|contact_id| contact_id.to_u32()), freshness)
|
(
|
||||||
|
contact.map(|contact_id| contact_id.to_u32()),
|
||||||
|
was_seen_recently,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
(None, ContactFreshness::Normal)
|
(None, false)
|
||||||
};
|
};
|
||||||
|
|
||||||
let color = color_int_to_hex_string(chat.get_color(ctx).await?);
|
let color = color_int_to_hex_string(chat.get_color(ctx).await?);
|
||||||
@@ -153,6 +157,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
summary_text1,
|
summary_text1,
|
||||||
summary_text2,
|
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_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_encrypted: chat.is_encrypted(ctx).await?,
|
is_encrypted: chat.is_encrypted(ctx).await?,
|
||||||
is_group: chat.get_type() == Chattype::Group,
|
is_group: chat.get_type() == Chattype::Group,
|
||||||
fresh_message_counter,
|
fresh_message_counter,
|
||||||
@@ -165,7 +170,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
is_muted: chat.is_muted(),
|
is_muted: chat.is_muted(),
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
dm_chat_contact,
|
dm_chat_contact,
|
||||||
freshness,
|
was_seen_recently,
|
||||||
last_message_type: message_type,
|
last_message_type: message_type,
|
||||||
last_message_id: last_msgid.map(|id| id.to_u32()),
|
last_message_id: last_msgid.map(|id| id.to_u32()),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use deltachat::contact;
|
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::key::{DcKey, SignedPublicKey};
|
use deltachat::key::{DcKey, SignedPublicKey};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -7,27 +6,6 @@ use typescript_type_def::TypeDef;
|
|||||||
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
|
|
||||||
/// Freshness of a contact, based on when it was last seen.
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
|
||||||
pub enum ContactFreshness {
|
|
||||||
/// Contact shall not be highlighted.
|
|
||||||
Normal,
|
|
||||||
/// Contact was seen recently.
|
|
||||||
RecentlySeen,
|
|
||||||
/// Contact was not seen for a long time.
|
|
||||||
Old,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<contact::Freshness> for ContactFreshness {
|
|
||||||
fn from(freshness: contact::Freshness) -> Self {
|
|
||||||
match freshness {
|
|
||||||
contact::Freshness::Normal => ContactFreshness::Normal,
|
|
||||||
contact::Freshness::RecentlySeen => ContactFreshness::RecentlySeen,
|
|
||||||
contact::Freshness::Old => ContactFreshness::Old,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Contact", rename_all = "camelCase")]
|
#[serde(rename = "Contact", rename_all = "camelCase")]
|
||||||
pub struct ContactObject {
|
pub struct ContactObject {
|
||||||
@@ -39,6 +17,7 @@ pub struct ContactObject {
|
|||||||
id: u32,
|
id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
profile_image: Option<String>, // BLOBS
|
profile_image: Option<String>, // BLOBS
|
||||||
|
name_and_addr: String,
|
||||||
is_blocked: bool,
|
is_blocked: bool,
|
||||||
|
|
||||||
/// Is the contact a key contact.
|
/// Is the contact a key contact.
|
||||||
@@ -52,9 +31,40 @@ pub struct ContactObject {
|
|||||||
/// e.g. if we just scanned the fingerprint from a QR code.
|
/// e.g. if we just scanned the fingerprint from a QR code.
|
||||||
e2ee_avail: bool,
|
e2ee_avail: bool,
|
||||||
|
|
||||||
|
/// True if the contact
|
||||||
|
/// can be added to protected chats
|
||||||
|
/// because SELF and contact have verified their fingerprints in both directions.
|
||||||
|
///
|
||||||
|
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
|
||||||
|
is_verified: bool,
|
||||||
|
|
||||||
|
/// The contact ID that verified a contact.
|
||||||
|
///
|
||||||
|
/// As verifier may be unknown,
|
||||||
|
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
|
||||||
|
///
|
||||||
|
/// UI should display the information in the contact's profile as follows:
|
||||||
|
///
|
||||||
|
/// - If `verifierId` != 0,
|
||||||
|
/// display text "Introduced by ..."
|
||||||
|
/// with the name of the contact.
|
||||||
|
/// Prefix the text by a green checkmark.
|
||||||
|
///
|
||||||
|
/// - If `verifierId` == 0 and `isVerified` != 0,
|
||||||
|
/// display "Introduced" prefixed by a green checkmark.
|
||||||
|
///
|
||||||
|
/// - if `verifierId` == 0 and `isVerified` == 0,
|
||||||
|
/// display nothing
|
||||||
|
///
|
||||||
|
/// This contains the contact ID of the verifier.
|
||||||
|
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
|
||||||
|
/// If it is None/Null, we don't have verifier information or
|
||||||
|
/// the contact is not verified.
|
||||||
|
verifier_id: Option<u32>,
|
||||||
|
|
||||||
/// the contact's last seen timestamp
|
/// the contact's last seen timestamp
|
||||||
last_seen: i64,
|
last_seen: i64,
|
||||||
freshness: ContactFreshness,
|
was_seen_recently: bool,
|
||||||
|
|
||||||
/// If the contact is a bot.
|
/// If the contact is a bot.
|
||||||
is_bot: bool,
|
is_bot: bool,
|
||||||
@@ -69,6 +79,14 @@ impl ContactObject {
|
|||||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
let is_verified = contact.is_verified(context).await?;
|
||||||
|
|
||||||
|
let verifier_id = contact
|
||||||
|
.get_verifier_id(context)
|
||||||
|
.await?
|
||||||
|
.flatten()
|
||||||
|
.map(|contact_id| contact_id.to_u32());
|
||||||
|
|
||||||
Ok(ContactObject {
|
Ok(ContactObject {
|
||||||
address: contact.get_addr().to_owned(),
|
address: contact.get_addr().to_owned(),
|
||||||
color: color_int_to_hex_string(contact.get_color()),
|
color: color_int_to_hex_string(contact.get_color()),
|
||||||
@@ -78,11 +96,14 @@ impl ContactObject {
|
|||||||
id: contact.id.to_u32(),
|
id: contact.id.to_u32(),
|
||||||
name: contact.get_name().to_owned(),
|
name: contact.get_name().to_owned(),
|
||||||
profile_image, //BLOBS
|
profile_image, //BLOBS
|
||||||
|
name_and_addr: contact.get_name_n_addr(),
|
||||||
is_blocked: contact.is_blocked(),
|
is_blocked: contact.is_blocked(),
|
||||||
is_key_contact: contact.is_key_contact(),
|
is_key_contact: contact.is_key_contact(),
|
||||||
e2ee_avail: contact.e2ee_avail(context).await?,
|
e2ee_avail: contact.e2ee_avail(context).await?,
|
||||||
|
is_verified,
|
||||||
|
verifier_id,
|
||||||
last_seen: contact.last_seen(),
|
last_seen: contact.last_seen(),
|
||||||
freshness: contact.get_freshness().into(),
|
was_seen_recently: contact.was_seen_recently(),
|
||||||
is_bot: contact.is_bot(),
|
is_bot: contact.is_bot(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ pub enum EventType {
|
|||||||
/// Emitted when an IMAP message has been marked as deleted
|
/// Emitted when an IMAP message has been marked as deleted
|
||||||
ImapMessageDeleted { msg: String },
|
ImapMessageDeleted { msg: String },
|
||||||
|
|
||||||
|
/// Emitted when an IMAP message has been moved
|
||||||
|
ImapMessageMoved { msg: String },
|
||||||
|
|
||||||
/// Emitted before going into IDLE on the Inbox folder.
|
/// Emitted before going into IDLE on the Inbox folder.
|
||||||
ImapInboxIdle,
|
ImapInboxIdle,
|
||||||
|
|
||||||
@@ -200,17 +203,6 @@ pub enum EventType {
|
|||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Like [`EventType::MsgRead`], but also fires on subsequent MDNs,
|
|
||||||
/// if there are multiple receivers, i.e. in groups and channels.
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
MsgReadCountChanged {
|
|
||||||
/// ID of the chat which the message belongs to.
|
|
||||||
chat_id: u32,
|
|
||||||
|
|
||||||
/// ID of the message that was read.
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A single message was deleted.
|
/// A single message was deleted.
|
||||||
///
|
///
|
||||||
/// This event means that the message will no longer appear in the messagelist.
|
/// This event means that the message will no longer appear in the messagelist.
|
||||||
@@ -258,15 +250,6 @@ pub enum EventType {
|
|||||||
chat_id: u32,
|
chat_id: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// The list of pinned messages for the chat has changed.
|
|
||||||
///
|
|
||||||
/// Some message got pinned, or pinned message is unpinned or deleted.
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
PinnedMessagesChanged {
|
|
||||||
/// ID of the chat where the list of pinned messages changed.
|
|
||||||
chat_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Contact(s) created, renamed, blocked or deleted.
|
/// Contact(s) created, renamed, blocked or deleted.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ContactsChanged {
|
ContactsChanged {
|
||||||
@@ -343,7 +326,8 @@ pub enum EventType {
|
|||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
|
|
||||||
/// Progress as:
|
/// Progress as:
|
||||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "introducing myself."
|
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||||
|
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||||
/// 1000=vg-member-added/vc-contact-confirm received
|
/// 1000=vg-member-added/vc-contact-confirm received
|
||||||
progress: u16,
|
progress: u16,
|
||||||
},
|
},
|
||||||
@@ -399,15 +383,11 @@ pub enum EventType {
|
|||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Tells that a background fetch call is done:
|
/// Tells that the Background fetch was completed (or timed out).
|
||||||
/// the fetch completed, timed out, was stopped or was not started.
|
/// This event acts as a marker, when you reach this event you can be sure
|
||||||
|
/// that all events emitted during the background fetch were processed.
|
||||||
///
|
///
|
||||||
/// For the call that started the fetch, this event acts as a marker:
|
/// This event is only emitted by the account manager
|
||||||
/// all events emitted during the fetch were processed once it is reached.
|
|
||||||
/// A call made while another background fetch is running gets the event immediately,
|
|
||||||
/// and the running fetch keeps emitting events until its own marker.
|
|
||||||
///
|
|
||||||
/// This event is only emitted by the account manager.
|
|
||||||
AccountsBackgroundFetchDone,
|
AccountsBackgroundFetchDone,
|
||||||
/// Inform that set of chats or the order of the chats in the chatlist has changed.
|
/// Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||||
///
|
///
|
||||||
@@ -487,9 +467,9 @@ pub enum EventType {
|
|||||||
///
|
///
|
||||||
/// UI should update the list.
|
/// UI should update the list.
|
||||||
///
|
///
|
||||||
/// The event is emitted on the device modifying
|
/// This event is emitted when transport
|
||||||
/// the transports as well as on other devices
|
/// synchronization messages arrives,
|
||||||
/// applying the synced change.
|
/// but not when the UI modifies the transport list by itself.
|
||||||
TransportsModified,
|
TransportsModified,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,6 +482,7 @@ impl From<CoreEventType> for EventType {
|
|||||||
CoreEventType::ImapConnected(msg) => ImapConnected { msg },
|
CoreEventType::ImapConnected(msg) => ImapConnected { msg },
|
||||||
CoreEventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
|
CoreEventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
|
||||||
CoreEventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
|
CoreEventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
|
||||||
|
CoreEventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
|
||||||
CoreEventType::ImapInboxIdle => ImapInboxIdle,
|
CoreEventType::ImapInboxIdle => ImapInboxIdle,
|
||||||
CoreEventType::NewBlobFile(file) => NewBlobFile { file },
|
CoreEventType::NewBlobFile(file) => NewBlobFile { file },
|
||||||
CoreEventType::DeletedBlobFile(file) => DeletedBlobFile { file },
|
CoreEventType::DeletedBlobFile(file) => DeletedBlobFile { file },
|
||||||
@@ -521,9 +502,6 @@ impl From<CoreEventType> for EventType {
|
|||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
contact_id: contact_id.to_u32(),
|
contact_id: contact_id.to_u32(),
|
||||||
},
|
},
|
||||||
CoreEventType::PinnedMessagesChanged { chat_id } => PinnedMessagesChanged {
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
},
|
|
||||||
CoreEventType::IncomingReaction {
|
CoreEventType::IncomingReaction {
|
||||||
chat_id,
|
chat_id,
|
||||||
contact_id,
|
contact_id,
|
||||||
@@ -568,10 +546,6 @@ impl From<CoreEventType> for EventType {
|
|||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
CoreEventType::MsgReadCountChanged { chat_id, msg_id } => MsgReadCountChanged {
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
msg_id: msg_id.to_u32(),
|
|
||||||
},
|
|
||||||
CoreEventType::MsgDeleted { chat_id, msg_id } => MsgDeleted {
|
CoreEventType::MsgDeleted { chat_id, msg_id } => MsgDeleted {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ pub struct HttpResponse {
|
|||||||
|
|
||||||
impl From<CoreHttpResponse> for HttpResponse {
|
impl From<CoreHttpResponse> for HttpResponse {
|
||||||
fn from(response: CoreHttpResponse) -> Self {
|
fn from(response: CoreHttpResponse) -> Self {
|
||||||
use base64::{Engine as _, engine::general_purpose};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob);
|
let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob);
|
||||||
let mimetype = response.mimetype;
|
let mimetype = response.mimetype;
|
||||||
let encoding = response.encoding;
|
let encoding = response.encoding;
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ use serde::Deserialize;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use yerpc::TypeDef;
|
use yerpc::TypeDef;
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TransportListEntry {
|
||||||
|
/// The login data entered by the user.
|
||||||
|
pub param: EnteredLoginParam,
|
||||||
|
/// Whether this transport is set to 'unpublished'.
|
||||||
|
/// See `set_transport_unpublished` / `setTransportUnpublished` for details.
|
||||||
|
pub is_unpublished: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Login parameters entered by the user.
|
/// Login parameters entered by the user.
|
||||||
///
|
///
|
||||||
/// Usually it will be enough to only set `addr` and `password`,
|
/// Usually it will be enough to only set `addr` and `password`,
|
||||||
@@ -56,6 +66,19 @@ pub struct EnteredLoginParam {
|
|||||||
/// invalid hostnames.
|
/// invalid hostnames.
|
||||||
/// Default: Automatic
|
/// Default: Automatic
|
||||||
pub certificate_checks: Option<EnteredCertificateChecks>,
|
pub certificate_checks: Option<EnteredCertificateChecks>,
|
||||||
|
|
||||||
|
/// If true, login via OAUTH2 (not recommended anymore).
|
||||||
|
/// Default: false
|
||||||
|
pub oauth2: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<dc::TransportListEntry> for TransportListEntry {
|
||||||
|
fn from(transport: dc::TransportListEntry) -> Self {
|
||||||
|
TransportListEntry {
|
||||||
|
param: transport.param.into(),
|
||||||
|
is_unpublished: transport.is_unpublished,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
||||||
@@ -77,6 +100,7 @@ impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
|||||||
smtp_user: param.smtp.user.into_option(),
|
smtp_user: param.smtp.user.into_option(),
|
||||||
smtp_password: param.smtp.password.into_option(),
|
smtp_password: param.smtp.password.into_option(),
|
||||||
certificate_checks: certificate_checks.into_option(),
|
certificate_checks: certificate_checks.into_option(),
|
||||||
|
oauth2: param.oauth2.into_option(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +127,7 @@ impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
|
|||||||
password: param.smtp_password.unwrap_or_default(),
|
password: param.smtp_password.unwrap_or_default(),
|
||||||
},
|
},
|
||||||
certificate_checks: param.certificate_checks.unwrap_or_default().into(),
|
certificate_checks: param.certificate_checks.unwrap_or_default().into(),
|
||||||
oauth2: false,
|
oauth2: param.oauth2.unwrap_or_default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,8 +174,9 @@ impl From<Socket> for dc::Socket {
|
|||||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
|
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum EnteredCertificateChecks {
|
pub enum EnteredCertificateChecks {
|
||||||
/// `Automatic` means strict certificate checks,
|
/// `Automatic` means that provider database setting should be taken.
|
||||||
/// unless a legacy-domain override disables them.
|
/// If there is no provider database setting for certificate checks,
|
||||||
|
/// check certificates strictly.
|
||||||
#[default]
|
#[default]
|
||||||
Automatic,
|
Automatic,
|
||||||
|
|
||||||
|
|||||||
@@ -103,9 +103,6 @@ pub struct MessageObject {
|
|||||||
|
|
||||||
saved_message_id: Option<u32>,
|
saved_message_id: Option<u32>,
|
||||||
|
|
||||||
is_pinned: bool,
|
|
||||||
|
|
||||||
/// `None` when there are no reactions.
|
|
||||||
reactions: Option<JsonrpcReactions>,
|
reactions: Option<JsonrpcReactions>,
|
||||||
|
|
||||||
vcard_contact: Option<VcardContact>,
|
vcard_contact: Option<VcardContact>,
|
||||||
@@ -266,7 +263,6 @@ impl MessageObject {
|
|||||||
.await?
|
.await?
|
||||||
.map(|id| id.to_u32()),
|
.map(|id| id.to_u32()),
|
||||||
|
|
||||||
is_pinned: message.is_pinned(),
|
|
||||||
reactions,
|
reactions,
|
||||||
|
|
||||||
vcard_contact: vcard_contacts.first().cloned(),
|
vcard_contact: vcard_contacts.first().cloned(),
|
||||||
@@ -394,11 +390,11 @@ pub enum SystemMessageType {
|
|||||||
LocationOnly,
|
LocationOnly,
|
||||||
InvalidUnencryptedMail,
|
InvalidUnencryptedMail,
|
||||||
|
|
||||||
/// Single chats info message telling that SecureJoin has started and the user should wait for it
|
/// 1:1 chats info message telling that SecureJoin has started and the user should wait for it
|
||||||
/// to complete.
|
/// to complete.
|
||||||
SecurejoinWait,
|
SecurejoinWait,
|
||||||
|
|
||||||
/// Single chats info message telling that SecureJoin is still running, but the user may already
|
/// 1:1 chats info message telling that SecureJoin is still running, but the user may already
|
||||||
/// send messages.
|
/// send messages.
|
||||||
SecurejoinWaitTimeout,
|
SecurejoinWaitTimeout,
|
||||||
|
|
||||||
@@ -429,8 +425,6 @@ pub enum SystemMessageType {
|
|||||||
|
|
||||||
CallAccepted,
|
CallAccepted,
|
||||||
CallEnded,
|
CallEnded,
|
||||||
MessagePinned,
|
|
||||||
MessageUnpinned,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||||
@@ -460,8 +454,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
|||||||
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
|
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
|
||||||
SystemMessage::CallAccepted => SystemMessageType::CallAccepted,
|
SystemMessage::CallAccepted => SystemMessageType::CallAccepted,
|
||||||
SystemMessage::CallEnded => SystemMessageType::CallEnded,
|
SystemMessage::CallEnded => SystemMessageType::CallEnded,
|
||||||
SystemMessage::MessagePinned => SystemMessageType::MessagePinned,
|
|
||||||
SystemMessage::MessageUnpinned => SystemMessageType::MessageUnpinned,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -734,9 +726,9 @@ impl From<deltachat::ephemeral::Timer> for EphemeralTimer {
|
|||||||
fn from(value: deltachat::ephemeral::Timer) -> Self {
|
fn from(value: deltachat::ephemeral::Timer) -> Self {
|
||||||
match value {
|
match value {
|
||||||
deltachat::ephemeral::Timer::Disabled => EphemeralTimer::Disabled,
|
deltachat::ephemeral::Timer::Disabled => EphemeralTimer::Disabled,
|
||||||
deltachat::ephemeral::Timer::Enabled { duration } => EphemeralTimer::Enabled {
|
deltachat::ephemeral::Timer::Enabled { duration } => {
|
||||||
duration: duration.get(),
|
EphemeralTimer::Enabled { duration }
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod account;
|
pub mod account;
|
||||||
pub mod appversions;
|
|
||||||
pub mod calls;
|
pub mod calls;
|
||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod chat_list;
|
pub mod chat_list;
|
||||||
@@ -9,6 +8,8 @@ pub mod http;
|
|||||||
pub mod location;
|
pub mod location;
|
||||||
pub mod login_param;
|
pub mod login_param;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
|
pub mod notify_state;
|
||||||
|
pub mod provider_info;
|
||||||
pub mod qr;
|
pub mod qr;
|
||||||
pub mod reactions;
|
pub mod reactions;
|
||||||
pub mod webxdc;
|
pub mod webxdc;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use deltachat::push::NotifyState;
|
||||||
|
use serde::Serialize;
|
||||||
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename = "NotifyState")]
|
||||||
|
pub enum JsonrpcNotifyState {
|
||||||
|
/// Not subscribed to push notifications.
|
||||||
|
NotConnected,
|
||||||
|
|
||||||
|
/// Subscribed to heartbeat push notifications.
|
||||||
|
Heartbeat,
|
||||||
|
|
||||||
|
/// Subscribed to push notifications for new messages.
|
||||||
|
Connected,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NotifyState> for JsonrpcNotifyState {
|
||||||
|
fn from(state: NotifyState) -> Self {
|
||||||
|
match state {
|
||||||
|
NotifyState::NotConnected => Self::NotConnected,
|
||||||
|
NotifyState::Heartbeat => Self::Heartbeat,
|
||||||
|
NotifyState::Connected => Self::Connected,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use deltachat::provider::Provider;
|
||||||
|
use num_traits::cast::ToPrimitive;
|
||||||
|
use serde::Serialize;
|
||||||
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ProviderInfo {
|
||||||
|
/// Unique ID, corresponding to provider database filename.
|
||||||
|
pub id: String,
|
||||||
|
pub before_login_hint: String,
|
||||||
|
pub overview_page: String,
|
||||||
|
pub status: u32, // in reality this is an enum, but for simplicity and because it gets converted into a number anyway, we use an u32 here.
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProviderInfo {
|
||||||
|
pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> {
|
||||||
|
provider.map(|p| ProviderInfo {
|
||||||
|
id: p.id.to_owned(),
|
||||||
|
before_login_hint: p.before_login_hint.to_owned(),
|
||||||
|
overview_page: p.overview_page.to_owned(),
|
||||||
|
status: p.status.to_u32().unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ use typescript_type_def::TypeDef;
|
|||||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||||
#[serde(tag = "kind")]
|
#[serde(tag = "kind")]
|
||||||
pub enum QrObject {
|
pub enum QrObject {
|
||||||
/// Ask the user whether to start chatting with the contact.
|
/// Ask the user whether to verify the contact.
|
||||||
///
|
///
|
||||||
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
|
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
|
||||||
AskVerifyContact {
|
AskVerifyContact {
|
||||||
@@ -61,13 +61,23 @@ pub enum QrObject {
|
|||||||
/// Whether the inviter supports the new Securejoin v3 protocol
|
/// Whether the inviter supports the new Securejoin v3 protocol
|
||||||
is_v3: bool,
|
is_v3: bool,
|
||||||
},
|
},
|
||||||
/// Contact fingerprint matches.
|
/// Contact fingerprint is verified.
|
||||||
///
|
///
|
||||||
/// Ask the user if they want to start chatting.
|
/// Ask the user if they want to start chatting.
|
||||||
FprOk {
|
FprOk {
|
||||||
/// Contact ID.
|
/// Contact ID.
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
},
|
},
|
||||||
|
/// Scanned fingerprint does not match the last seen fingerprint.
|
||||||
|
FprMismatch {
|
||||||
|
/// Contact ID.
|
||||||
|
contact_id: Option<u32>,
|
||||||
|
},
|
||||||
|
/// The scanned QR code contains a fingerprint but no e-mail address.
|
||||||
|
FprWithoutAddr {
|
||||||
|
/// Key fingerprint.
|
||||||
|
fingerprint: String,
|
||||||
|
},
|
||||||
/// Ask the user if they want to create an account on the given domain.
|
/// Ask the user if they want to create an account on the given domain.
|
||||||
Account {
|
Account {
|
||||||
/// Server domain name.
|
/// Server domain name.
|
||||||
@@ -226,7 +236,6 @@ impl From<Qr> for QrObject {
|
|||||||
invitenumber,
|
invitenumber,
|
||||||
authcode,
|
authcode,
|
||||||
is_v3,
|
is_v3,
|
||||||
..
|
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.human_readable();
|
let fingerprint = fingerprint.human_readable();
|
||||||
@@ -246,7 +255,6 @@ impl From<Qr> for QrObject {
|
|||||||
invitenumber,
|
invitenumber,
|
||||||
authcode,
|
authcode,
|
||||||
is_v3,
|
is_v3,
|
||||||
..
|
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.human_readable();
|
let fingerprint = fingerprint.human_readable();
|
||||||
@@ -268,7 +276,6 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
invitenumber,
|
invitenumber,
|
||||||
is_v3,
|
is_v3,
|
||||||
..
|
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.human_readable();
|
let fingerprint = fingerprint.human_readable();
|
||||||
@@ -286,6 +293,11 @@ impl From<Qr> for QrObject {
|
|||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
QrObject::FprOk { contact_id }
|
QrObject::FprOk { contact_id }
|
||||||
}
|
}
|
||||||
|
Qr::FprMismatch { contact_id } => {
|
||||||
|
let contact_id = contact_id.map(|contact_id| contact_id.to_u32());
|
||||||
|
QrObject::FprMismatch { contact_id }
|
||||||
|
}
|
||||||
|
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
|
||||||
Qr::Account { domain } => QrObject::Account { domain },
|
Qr::Account { domain } => QrObject::Account { domain },
|
||||||
Qr::Backup2 {
|
Qr::Backup2 {
|
||||||
ref node_addr,
|
ref node_addr,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use deltachat::contact::ContactId;
|
||||||
use deltachat::reaction::Reactions;
|
use deltachat::reaction::Reactions;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
@@ -23,11 +24,8 @@ pub struct JsonrpcReaction {
|
|||||||
#[serde(rename = "Reactions", rename_all = "camelCase")]
|
#[serde(rename = "Reactions", rename_all = "camelCase")]
|
||||||
pub struct JsonrpcReactions {
|
pub struct JsonrpcReactions {
|
||||||
/// Map from a contact to it's reaction to message.
|
/// Map from a contact to it's reaction to message.
|
||||||
///
|
|
||||||
/// There is only a single reaction per contact,
|
/// There is only a single reaction per contact,
|
||||||
/// but this contains a list of reactions for historical reasons.
|
/// but this contains a list of reactions for historical reasons.
|
||||||
///
|
|
||||||
/// For channels subscribers, this map is empty or contains `ContactId::SELF` only.
|
|
||||||
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
||||||
/// Unique reactions and their count, sorted in descending order.
|
/// Unique reactions and their count, sorted in descending order.
|
||||||
reactions: Vec<JsonrpcReaction>,
|
reactions: Vec<JsonrpcReaction>,
|
||||||
@@ -36,24 +34,30 @@ pub struct JsonrpcReactions {
|
|||||||
impl From<Reactions> for JsonrpcReactions {
|
impl From<Reactions> for JsonrpcReactions {
|
||||||
fn from(reactions: Reactions) -> Self {
|
fn from(reactions: Reactions) -> Self {
|
||||||
let reactions_by_contact: BTreeMap<u32, Vec<String>> = reactions
|
let reactions_by_contact: BTreeMap<u32, Vec<String>> = reactions
|
||||||
.by_contact
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(key, value)| (key.to_u32(), vec![value.as_str().to_string()]))
|
.map(|(key, value)| (key.to_u32(), vec![value.as_str().to_string()]))
|
||||||
.collect();
|
.collect();
|
||||||
|
let self_reaction = reactions_by_contact.get(&ContactId::SELF.to_u32());
|
||||||
|
|
||||||
let reactions = reactions
|
let mut reactions_v = Vec::new();
|
||||||
.frequencies
|
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
|
||||||
.into_iter()
|
let is_from_self = if let Some(self_reaction) = self_reaction {
|
||||||
.map(|entry| JsonrpcReaction {
|
self_reaction.contains(&emoji)
|
||||||
emoji: entry.reaction.as_str().to_string(),
|
} else {
|
||||||
count: entry.count,
|
false
|
||||||
is_from_self: entry.is_from_self,
|
};
|
||||||
})
|
|
||||||
.collect();
|
let reaction = JsonrpcReaction {
|
||||||
|
emoji,
|
||||||
|
count,
|
||||||
|
is_from_self,
|
||||||
|
};
|
||||||
|
reactions_v.push(reaction)
|
||||||
|
}
|
||||||
|
|
||||||
JsonrpcReactions {
|
JsonrpcReactions {
|
||||||
reactions_by_contact,
|
reactions_by_contact,
|
||||||
reactions,
|
reactions: reactions_v,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -54,5 +54,5 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "dist/deltachat.d.ts",
|
"types": "dist/deltachat.d.ts",
|
||||||
"version": "2.64.0-dev"
|
"version": "2.51.0-dev"
|
||||||
}
|
}
|
||||||
+1
@@ -44,6 +44,7 @@ const constants = data
|
|||||||
key.startsWith("DC_CERTCK_") ||
|
key.startsWith("DC_CERTCK_") ||
|
||||||
key.startsWith("DC_SOCKET_") ||
|
key.startsWith("DC_SOCKET_") ||
|
||||||
key.startsWith("DC_LP_AUTH_") ||
|
key.startsWith("DC_LP_AUTH_") ||
|
||||||
|
key.startsWith("DC_PUSH_") ||
|
||||||
key.startsWith("DC_TEXT1_") ||
|
key.startsWith("DC_TEXT1_") ||
|
||||||
key.startsWith("DC_CHAT_TYPE")
|
key.startsWith("DC_CHAT_TYPE")
|
||||||
);
|
);
|
||||||
+17
@@ -148,6 +148,23 @@ describe("online tests", function () {
|
|||||||
expect(message2.text).equal("super secret message");
|
expect(message2.text).equal("super secret message");
|
||||||
expect(message2.showPadlock).equal(true);
|
expect(message2.showPadlock).equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("get provider info for example.com", async () => {
|
||||||
|
const acc = await dc.rpc.addAccount();
|
||||||
|
const info = await dc.rpc.getProviderInfo(acc, "example.com");
|
||||||
|
expect(info).to.be.not.null;
|
||||||
|
expect(info?.overviewPage).to.equal(
|
||||||
|
"https://providers.delta.chat/example-com",
|
||||||
|
);
|
||||||
|
expect(info?.status).to.equal(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("get provider info - domain and email should give same result", async () => {
|
||||||
|
const acc = await dc.rpc.addAccount();
|
||||||
|
const info_domain = await dc.rpc.getProviderInfo(acc, "example.com");
|
||||||
|
const info_email = await dc.rpc.getProviderInfo(acc, "hi@example.com");
|
||||||
|
expect(info_email).to.deep.equal(info_domain);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function waitForEvent<T extends DcEvent["kind"]>(
|
async function waitForEvent<T extends DcEvent["kind"]>(
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "ratelimit"
|
name = "ratelimit"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
description = "Token bucket implementation"
|
description = "Token bucket implementation"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-repl"
|
name = "deltachat-repl"
|
||||||
version = "2.64.0-dev"
|
version = "2.51.0-dev"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
repository = "https://github.com/chatmail/core"
|
repository = "https://github.com/chatmail/core"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
deltachat = { workspace = true, features = ["internals"]}
|
deltachat = { workspace = true, features = ["internals"]}
|
||||||
dirs = "7"
|
dirs = "6"
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
qr2term = "0.3.3"
|
qr2term = "0.3.3"
|
||||||
|
|||||||
+100
-62
@@ -5,10 +5,9 @@ use std::path::Path;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Result, bail, ensure};
|
use anyhow::{bail, ensure, Result};
|
||||||
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
|
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
|
||||||
use deltachat::chatlist::*;
|
use deltachat::chatlist::*;
|
||||||
use deltachat::config;
|
|
||||||
use deltachat::constants::*;
|
use deltachat::constants::*;
|
||||||
use deltachat::contact::*;
|
use deltachat::contact::*;
|
||||||
use deltachat::context::*;
|
use deltachat::context::*;
|
||||||
@@ -20,13 +19,63 @@ use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
|||||||
use deltachat::mimeparser::SystemMessage;
|
use deltachat::mimeparser::SystemMessage;
|
||||||
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
|
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
|
||||||
use deltachat::qr::*;
|
use deltachat::qr::*;
|
||||||
use deltachat::qr_code_generator::{create_qr_svg, get_securejoin_qr_svg};
|
use deltachat::qr_code_generator::create_qr_svg;
|
||||||
use deltachat::reaction::send_reaction;
|
use deltachat::reaction::send_reaction;
|
||||||
use deltachat::receive_imf::*;
|
use deltachat::receive_imf::*;
|
||||||
use deltachat::sql;
|
use deltachat::sql;
|
||||||
use deltachat::tools::*;
|
use deltachat::tools::*;
|
||||||
|
use deltachat::{config, provider};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
|
/// Reset database tables.
|
||||||
|
/// Argument is a bitmask, executing single or multiple actions in one call.
|
||||||
|
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||||
|
async fn reset_tables(context: &Context, bits: i32) {
|
||||||
|
println!("Resetting tables ({bits})...");
|
||||||
|
if 0 != bits & 4 {
|
||||||
|
context
|
||||||
|
.sql()
|
||||||
|
.execute("DELETE FROM keypairs;", ())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
println!("(4) Private keypairs reset.");
|
||||||
|
}
|
||||||
|
if 0 != bits & 8 {
|
||||||
|
context
|
||||||
|
.sql()
|
||||||
|
.execute("DELETE FROM contacts WHERE id>9;", ())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
context
|
||||||
|
.sql()
|
||||||
|
.execute("DELETE FROM chats WHERE id>9;", ())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
context
|
||||||
|
.sql()
|
||||||
|
.execute("DELETE FROM chats_contacts;", ())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
context
|
||||||
|
.sql()
|
||||||
|
.execute("DELETE FROM msgs WHERE id>9;", ())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
context
|
||||||
|
.sql()
|
||||||
|
.execute(
|
||||||
|
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
context.sql().config_cache().write().await.clear();
|
||||||
|
println!("(8) Rest but server config reset.");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.emit_msgs_changed_without_ids();
|
||||||
|
}
|
||||||
|
|
||||||
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||||
let data = read_file(context, filename).await?;
|
let data = read_file(context, filename).await?;
|
||||||
|
|
||||||
@@ -179,7 +228,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
|||||||
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
|
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
|
||||||
let mut lines_out = 0;
|
let mut lines_out = 0;
|
||||||
for &msg_id in msglist {
|
for &msg_id in msglist {
|
||||||
if msg_id == MsgId::DAYMARKER {
|
if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {
|
||||||
println!(
|
println!(
|
||||||
"--------------------------------------------------------------------------------"
|
"--------------------------------------------------------------------------------"
|
||||||
);
|
);
|
||||||
@@ -210,13 +259,19 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
|
|||||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||||
let name = contact.get_display_name();
|
let name = contact.get_display_name();
|
||||||
let addr = contact.get_addr();
|
let addr = contact.get_addr();
|
||||||
|
let verified_str = if contact.is_verified(context).await? {
|
||||||
|
" √"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
let line = format!(
|
let line = format!(
|
||||||
"{} <{}>",
|
"{}{} <{}>",
|
||||||
if !name.is_empty() {
|
if !name.is_empty() {
|
||||||
name
|
name
|
||||||
} else {
|
} else {
|
||||||
"<name unset>"
|
"<name unset>"
|
||||||
},
|
},
|
||||||
|
verified_str,
|
||||||
if !addr.is_empty() { addr } else { "addr unset" }
|
if !addr.is_empty() { addr } else { "addr unset" }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -255,6 +310,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
export-keys\n\
|
export-keys\n\
|
||||||
import-keys <key-file>\n\
|
import-keys <key-file>\n\
|
||||||
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
||||||
|
reset <flags>\n\
|
||||||
stop\n\
|
stop\n\
|
||||||
============================================="
|
============================================="
|
||||||
),
|
),
|
||||||
@@ -263,6 +319,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
info\n\
|
info\n\
|
||||||
set <configuration-key> [<value>]\n\
|
set <configuration-key> [<value>]\n\
|
||||||
get <configuration-key>\n\
|
get <configuration-key>\n\
|
||||||
|
oauth2\n\
|
||||||
configure\n\
|
configure\n\
|
||||||
connect\n\
|
connect\n\
|
||||||
disconnect\n\
|
disconnect\n\
|
||||||
@@ -339,6 +396,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
joinqr <qr-content>\n\
|
joinqr <qr-content>\n\
|
||||||
setqr <qr-content>\n\
|
setqr <qr-content>\n\
|
||||||
createqrsvg <qr-content>\n\
|
createqrsvg <qr-content>\n\
|
||||||
|
providerinfo <addr>\n\
|
||||||
fileinfo <file>\n\
|
fileinfo <file>\n\
|
||||||
estimatedeletion <seconds>\n\
|
estimatedeletion <seconds>\n\
|
||||||
clear -- clear screen\n\
|
clear -- clear screen\n\
|
||||||
@@ -346,57 +404,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
============================================="
|
============================================="
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"connect" => {
|
|
||||||
context.start_io().await;
|
|
||||||
}
|
|
||||||
"disconnect" => {
|
|
||||||
context.stop_io().await;
|
|
||||||
}
|
|
||||||
"fetch" => {
|
|
||||||
context.background_fetch().await?;
|
|
||||||
}
|
|
||||||
"configure" => {
|
|
||||||
context.configure().await?;
|
|
||||||
}
|
|
||||||
"has-backup" => {
|
"has-backup" => {
|
||||||
has_backup(&context, blobdir).await?;
|
has_backup(&context, blobdir).await?;
|
||||||
}
|
}
|
||||||
"clear" => {
|
|
||||||
println!("\n\n\n");
|
|
||||||
print!("\x1b[1;1H\x1b[2J");
|
|
||||||
}
|
|
||||||
"getqr" | "getbadqr" => {
|
|
||||||
context.start_io().await;
|
|
||||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
|
||||||
let mut qr = deltachat::securejoin::get_securejoin_qr(&context, group).await?;
|
|
||||||
if !qr.is_empty() {
|
|
||||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
|
||||||
qr.replace_range(12..22, "0000000000")
|
|
||||||
}
|
|
||||||
println!("{qr}");
|
|
||||||
qr2term::print_qr(qr.as_str())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"getqrsvg" => {
|
|
||||||
context.start_io().await;
|
|
||||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
|
||||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
|
||||||
match get_securejoin_qr_svg(&context, group).await {
|
|
||||||
Ok(svg) => {
|
|
||||||
fs::write(&file, svg).await?;
|
|
||||||
println!("QR code svg written to: {file:#?}");
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
bail!("Failed to get QR code svg: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"joinqr" => {
|
|
||||||
context.start_io().await;
|
|
||||||
if !arg0.is_empty() {
|
|
||||||
deltachat::securejoin::join_securejoin(&context, arg1).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"export-backup" => {
|
"export-backup" => {
|
||||||
let dir = dirs::home_dir().unwrap_or_default();
|
let dir = dirs::home_dir().unwrap_or_default();
|
||||||
imex(
|
imex(
|
||||||
@@ -442,6 +452,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
"poke" => {
|
"poke" => {
|
||||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||||
}
|
}
|
||||||
|
"reset" => {
|
||||||
|
ensure!(
|
||||||
|
!arg1.is_empty(),
|
||||||
|
"Argument <bits> missing: 4=private keys, 8=rest but server config"
|
||||||
|
);
|
||||||
|
let bits: i32 = arg1.parse()?;
|
||||||
|
ensure!(bits < 16, "<bits> must be lower than 16.");
|
||||||
|
reset_tables(&context, bits).await;
|
||||||
|
}
|
||||||
"stop" => {
|
"stop" => {
|
||||||
context.stop_ongoing().await;
|
context.stop_ongoing().await;
|
||||||
}
|
}
|
||||||
@@ -599,7 +618,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
let sel_chat = sel_chat.as_ref().unwrap();
|
let sel_chat = sel_chat.as_ref().unwrap();
|
||||||
|
|
||||||
let time_start = std::time::SystemTime::now();
|
let time_start = std::time::SystemTime::now();
|
||||||
let msglist = chat::get_chat_msgs_ext(
|
let msglist = chat::get_chat_msgs_ex(
|
||||||
&context,
|
&context,
|
||||||
sel_chat.get_id(),
|
sel_chat.get_id(),
|
||||||
chat::MessageListOptions {
|
chat::MessageListOptions {
|
||||||
@@ -613,7 +632,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| match x {
|
.map(|x| match x {
|
||||||
ChatItem::Message { msg_id } => msg_id,
|
ChatItem::Message { msg_id } => msg_id,
|
||||||
ChatItem::DayMarker { .. } => MsgId::DAYMARKER,
|
ChatItem::DayMarker { .. } => MsgId::new(DC_MSG_ID_DAYMARKER),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -1102,11 +1121,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
let contact_id = ContactId::new(arg1.parse()?);
|
let contact_id = ContactId::new(arg1.parse()?);
|
||||||
let contact = Contact::get_by_id(&context, contact_id).await?;
|
let contact = Contact::get_by_id(&context, contact_id).await?;
|
||||||
let name = contact.get_display_name();
|
let name_n_addr = contact.get_name_n_addr();
|
||||||
let addr = contact.get_addr();
|
|
||||||
|
|
||||||
let mut res = format!(
|
let mut res = format!(
|
||||||
"Contact info for: {name} ({addr}):\nIcon: {}\n",
|
"Contact info for: {}:\nIcon: {}\n",
|
||||||
|
name_n_addr,
|
||||||
match contact.get_profile_image(&context).await? {
|
match contact.get_profile_image(&context).await? {
|
||||||
Some(image) => image.to_str().unwrap().to_string(),
|
Some(image) => image.to_str().unwrap().to_string(),
|
||||||
None => "NoIcon".to_string(),
|
None => "NoIcon".to_string(),
|
||||||
@@ -1186,14 +1205,33 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
fs::write(&file, svg).await?;
|
fs::write(&file, svg).await?;
|
||||||
println!("{file:#?} written.");
|
println!("{file:#?} written.");
|
||||||
}
|
}
|
||||||
|
"providerinfo" => {
|
||||||
|
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
||||||
|
match provider::get_provider_info(arg1) {
|
||||||
|
Some(info) => {
|
||||||
|
println!("Information for provider belonging to {arg1}:");
|
||||||
|
println!("status: {}", info.status as u32);
|
||||||
|
println!("before_login_hint: {}", info.before_login_hint);
|
||||||
|
println!("after_login_hint: {}", info.after_login_hint);
|
||||||
|
println!("overview_page: {}", info.overview_page);
|
||||||
|
for server in info.server.iter() {
|
||||||
|
println!("server: {}:{}", server.hostname, server.port,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("No information for provider belonging to {arg1} found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
"fileinfo" => {
|
"fileinfo" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||||
|
|
||||||
let Ok(buf) = read_file(&context, Path::new(arg1)).await else {
|
if let Ok(buf) = read_file(&context, Path::new(arg1)).await {
|
||||||
|
let (width, height) = get_filemeta(&buf)?;
|
||||||
|
println!("width={width}, height={height}");
|
||||||
|
} else {
|
||||||
bail!("Command failed.");
|
bail!("Command failed.");
|
||||||
};
|
}
|
||||||
let (width, height) = get_filemeta(&buf)?;
|
|
||||||
println!("width={width}, height={height}");
|
|
||||||
}
|
}
|
||||||
"estimatedeletion" => {
|
"estimatedeletion" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <seconds> missing");
|
ensure!(!arg1.is_empty(), "Argument <seconds> missing");
|
||||||
|
|||||||
+118
-19
@@ -9,10 +9,14 @@ extern crate deltachat;
|
|||||||
|
|
||||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||||
|
|
||||||
use anyhow::{Error, bail};
|
use anyhow::{bail, Error};
|
||||||
use deltachat::EventType;
|
|
||||||
use deltachat::chat::ChatId;
|
use deltachat::chat::ChatId;
|
||||||
|
use deltachat::config;
|
||||||
use deltachat::context::*;
|
use deltachat::context::*;
|
||||||
|
use deltachat::oauth2::*;
|
||||||
|
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||||
|
use deltachat::securejoin::*;
|
||||||
|
use deltachat::EventType;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use nu_ansi_term::Color;
|
use nu_ansi_term::Color;
|
||||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||||
@@ -23,6 +27,7 @@ use rustyline::validate::Validator;
|
|||||||
use rustyline::{
|
use rustyline::{
|
||||||
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyEvent,
|
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyEvent,
|
||||||
};
|
};
|
||||||
|
use tokio::fs;
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
@@ -144,7 +149,7 @@ impl Completer for DcHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const IMEX_COMMANDS: [&str; 9] = [
|
const IMEX_COMMANDS: [&str; 10] = [
|
||||||
"has-backup",
|
"has-backup",
|
||||||
"export-backup",
|
"export-backup",
|
||||||
"import-backup",
|
"import-backup",
|
||||||
@@ -153,13 +158,15 @@ const IMEX_COMMANDS: [&str; 9] = [
|
|||||||
"export-keys",
|
"export-keys",
|
||||||
"import-keys",
|
"import-keys",
|
||||||
"poke",
|
"poke",
|
||||||
|
"reset",
|
||||||
"stop",
|
"stop",
|
||||||
];
|
];
|
||||||
|
|
||||||
const DB_COMMANDS: [&str; 10] = [
|
const DB_COMMANDS: [&str; 11] = [
|
||||||
"info",
|
"info",
|
||||||
"set",
|
"set",
|
||||||
"get",
|
"get",
|
||||||
|
"oauth2",
|
||||||
"configure",
|
"configure",
|
||||||
"connect",
|
"connect",
|
||||||
"disconnect",
|
"disconnect",
|
||||||
@@ -169,7 +176,7 @@ const DB_COMMANDS: [&str; 10] = [
|
|||||||
"housekeeping",
|
"housekeeping",
|
||||||
];
|
];
|
||||||
|
|
||||||
const CHAT_COMMANDS: [&str; 38] = [
|
const CHAT_COMMANDS: [&str; 39] = [
|
||||||
"listchats",
|
"listchats",
|
||||||
"listarchived",
|
"listarchived",
|
||||||
"start-realtime",
|
"start-realtime",
|
||||||
@@ -178,6 +185,7 @@ const CHAT_COMMANDS: [&str; 38] = [
|
|||||||
"createchat",
|
"createchat",
|
||||||
"creategroup",
|
"creategroup",
|
||||||
"createbroadcast",
|
"createbroadcast",
|
||||||
|
"createprotected",
|
||||||
"addmember",
|
"addmember",
|
||||||
"removemember",
|
"removemember",
|
||||||
"groupname",
|
"groupname",
|
||||||
@@ -232,7 +240,7 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
|||||||
"import-vcard",
|
"import-vcard",
|
||||||
"make-vcard",
|
"make-vcard",
|
||||||
];
|
];
|
||||||
const MISC_COMMANDS: [&str; 13] = [
|
const MISC_COMMANDS: [&str; 14] = [
|
||||||
"getqr",
|
"getqr",
|
||||||
"getqrsvg",
|
"getqrsvg",
|
||||||
"getbadqr",
|
"getbadqr",
|
||||||
@@ -240,6 +248,7 @@ const MISC_COMMANDS: [&str; 13] = [
|
|||||||
"joinqr",
|
"joinqr",
|
||||||
"setqr",
|
"setqr",
|
||||||
"createqrsvg",
|
"createqrsvg",
|
||||||
|
"providerinfo",
|
||||||
"fileinfo",
|
"fileinfo",
|
||||||
"estimatedeletion",
|
"estimatedeletion",
|
||||||
"clear",
|
"clear",
|
||||||
@@ -261,11 +270,10 @@ impl Hinter for DcHelper {
|
|||||||
&CONTACT_COMMANDS[..],
|
&CONTACT_COMMANDS[..],
|
||||||
&MISC_COMMANDS[..],
|
&MISC_COMMANDS[..],
|
||||||
] {
|
] {
|
||||||
if let Some(entry) = cmds.iter().find(|el| el.starts_with(&line[..pos]))
|
if let Some(entry) = cmds.iter().find(|el| el.starts_with(&line[..pos])) {
|
||||||
&& *entry != line
|
if *entry != line && *entry != &line[..pos] {
|
||||||
&& *entry != &line[..pos]
|
return Some(entry[pos..].to_owned());
|
||||||
{
|
}
|
||||||
return Some(entry[pos..].to_owned());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,16 +357,23 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
Ok(line) => {
|
Ok(line) => {
|
||||||
// TODO: ignore "set mail_pw"
|
// TODO: ignore "set mail_pw"
|
||||||
rl.add_history_entry(line.as_str())?;
|
rl.add_history_entry(line.as_str())?;
|
||||||
let line = line.trim();
|
let should_continue = Handle::current().block_on(async {
|
||||||
if matches!(line, "exit" | "quit") {
|
match handle_cmd(line.trim(), ctx.clone(), &mut selected_chat).await {
|
||||||
println!("Exiting ...");
|
Ok(ExitResult::Continue) => true,
|
||||||
|
Ok(ExitResult::Exit) => {
|
||||||
|
println!("Exiting ...");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Error: {err:#}");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if !should_continue {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if let Err(err) =
|
|
||||||
Handle::current().block_on(cmdline(ctx.clone(), line, &mut selected_chat))
|
|
||||||
{
|
|
||||||
eprintln!("Error: {err:#}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
|
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
|
||||||
println!("Exiting...");
|
println!("Exiting...");
|
||||||
@@ -382,6 +397,90 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum ExitResult {
|
||||||
|
Continue,
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_cmd(
|
||||||
|
line: &str,
|
||||||
|
ctx: Context,
|
||||||
|
selected_chat: &mut ChatId,
|
||||||
|
) -> Result<ExitResult, Error> {
|
||||||
|
let mut args = line.splitn(2, ' ');
|
||||||
|
let arg0 = args.next().unwrap_or_default();
|
||||||
|
let arg1 = args.next().unwrap_or_default();
|
||||||
|
|
||||||
|
match arg0 {
|
||||||
|
"connect" => {
|
||||||
|
ctx.start_io().await;
|
||||||
|
}
|
||||||
|
"disconnect" => {
|
||||||
|
ctx.stop_io().await;
|
||||||
|
}
|
||||||
|
"fetch" => {
|
||||||
|
ctx.background_fetch().await?;
|
||||||
|
}
|
||||||
|
"configure" => {
|
||||||
|
ctx.configure().await?;
|
||||||
|
}
|
||||||
|
"oauth2" => {
|
||||||
|
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
||||||
|
if let Some(oauth2_url) =
|
||||||
|
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?
|
||||||
|
{
|
||||||
|
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
|
||||||
|
} else {
|
||||||
|
println!("OAuth2 not available for {addr}.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("oauth2: set addr first.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"clear" => {
|
||||||
|
println!("\n\n\n");
|
||||||
|
print!("\x1b[1;1H\x1b[2J");
|
||||||
|
}
|
||||||
|
"getqr" | "getbadqr" => {
|
||||||
|
ctx.start_io().await;
|
||||||
|
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||||
|
let mut qr = get_securejoin_qr(&ctx, group).await?;
|
||||||
|
if !qr.is_empty() {
|
||||||
|
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||||
|
qr.replace_range(12..22, "0000000000")
|
||||||
|
}
|
||||||
|
println!("{qr}");
|
||||||
|
qr2term::print_qr(qr.as_str())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"getqrsvg" => {
|
||||||
|
ctx.start_io().await;
|
||||||
|
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||||
|
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||||
|
match get_securejoin_qr_svg(&ctx, group).await {
|
||||||
|
Ok(svg) => {
|
||||||
|
fs::write(&file, svg).await?;
|
||||||
|
println!("QR code svg written to: {file:#?}");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
bail!("Failed to get QR code svg: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"joinqr" => {
|
||||||
|
ctx.start_io().await;
|
||||||
|
if !arg0.is_empty() {
|
||||||
|
join_securejoin(&ctx, arg1).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"exit" | "quit" => return Ok(ExitResult::Exit),
|
||||||
|
_ => cmdline(ctx.clone(), line, selected_chat).await?,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ExitResult::Continue)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Error> {
|
async fn main() -> Result<(), Error> {
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "deltachat-rpc-client"
|
name = "deltachat-rpc-client"
|
||||||
version = "2.64.0-dev"
|
version = "2.51.0-dev"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
|||||||
"""
|
"""
|
||||||
# You removed member a@b.
|
# You removed member a@b.
|
||||||
# You added member a@b.
|
# You added member a@b.
|
||||||
# You were removed by a@b.
|
# Member Me (x@y) removed by a@b.
|
||||||
# Member x@y added by a@b
|
# Member x@y added by a@b
|
||||||
# Member With space (tmp1@x.org) removed by tmp2@x.org.
|
# Member With space (tmp1@x.org) removed by tmp2@x.org.
|
||||||
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
|
||||||
@@ -36,15 +35,6 @@ class Account:
|
|||||||
if event_type is None or next_event.kind == event_type:
|
if event_type is None or next_event.kind == event_type:
|
||||||
return next_event
|
return next_event
|
||||||
|
|
||||||
def wait_for_realtime_data(self, msg_id: int) -> bytes:
|
|
||||||
"""Wait for the next realtime data received for the given webxdc message and return it."""
|
|
||||||
logging.info(f"account {self.id}: waiting for realtime data for msg {msg_id}")
|
|
||||||
while True:
|
|
||||||
event = self.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
|
|
||||||
if event.msg_id == msg_id:
|
|
||||||
logging.info(f"account {self.id}: got realtime data for msg {msg_id}: {event.data[:20]}")
|
|
||||||
return bytes(event.data)
|
|
||||||
|
|
||||||
def clear_all_events(self):
|
def clear_all_events(self):
|
||||||
"""Remove all queued-up events for a given account.
|
"""Remove all queued-up events for a given account.
|
||||||
|
|
||||||
@@ -139,18 +129,6 @@ class Account:
|
|||||||
"""Add a new transport using a QR code."""
|
"""Add a new transport using a QR code."""
|
||||||
yield self._rpc.add_transport_from_qr.future(self.id, qr)
|
yield self._rpc.add_transport_from_qr.future(self.id, qr)
|
||||||
|
|
||||||
@futuremethod
|
|
||||||
def init_transports(self, qr: Optional[str] = None):
|
|
||||||
"""Add an initial transport on the chatmail relay that answers fastest.
|
|
||||||
|
|
||||||
The profile then adds further ones in the background.
|
|
||||||
A ``DCACCOUNT:`` or ``DCLOGIN:`` ``qr`` code adds a single transport
|
|
||||||
while securejoin codes add the inviter's relays to the candidates.
|
|
||||||
|
|
||||||
Does nothing if the profile already has a transport.
|
|
||||||
"""
|
|
||||||
yield self._rpc.init_transports.future(self.id, qr)
|
|
||||||
|
|
||||||
def delete_transport(self, addr: str):
|
def delete_transport(self, addr: str):
|
||||||
"""Delete a transport."""
|
"""Delete a transport."""
|
||||||
self._rpc.delete_transport(self.id, addr)
|
self._rpc.delete_transport(self.id, addr)
|
||||||
@@ -162,10 +140,9 @@ class Account:
|
|||||||
return transports
|
return transports
|
||||||
|
|
||||||
def bring_online(self):
|
def bring_online(self):
|
||||||
"""Start I/O, wait until all transports became IDLE and drop the events seen so far."""
|
"""Start I/O and wait until IMAP becomes IDLE."""
|
||||||
self.start_io()
|
self.start_io()
|
||||||
self._rpc.wait_for_all_work_done(self.id)
|
self.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||||
self.clear_all_events()
|
|
||||||
|
|
||||||
def create_contact(self, obj: Union[int, str, Contact, "Account"], name: Optional[str] = None) -> Contact:
|
def create_contact(self, obj: Union[int, str, Contact, "Account"], name: Optional[str] = None) -> Contact:
|
||||||
"""Create a new Contact or return an existing one.
|
"""Create a new Contact or return an existing one.
|
||||||
@@ -205,7 +182,7 @@ class Account:
|
|||||||
return [Contact(self, contact_id) for contact_id in contact_ids]
|
return [Contact(self, contact_id) for contact_id in contact_ids]
|
||||||
|
|
||||||
def create_chat(self, account: "Account") -> Chat:
|
def create_chat(self, account: "Account") -> Chat:
|
||||||
"""Create a single chat with another account."""
|
"""Create a 1:1 chat with another account."""
|
||||||
return self.create_contact(account).create_chat()
|
return self.create_contact(account).create_chat()
|
||||||
|
|
||||||
def get_device_chat(self) -> Chat:
|
def get_device_chat(self) -> Chat:
|
||||||
@@ -241,7 +218,7 @@ class Account:
|
|||||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||||
|
|
||||||
def get_chat_by_contact(self, contact: Union[int, Contact]) -> Optional[Chat]:
|
def get_chat_by_contact(self, contact: Union[int, Contact]) -> Optional[Chat]:
|
||||||
"""Return single chat for a contact if it exists."""
|
"""Return 1:1 chat for a contact if it exists."""
|
||||||
if isinstance(contact, Contact):
|
if isinstance(contact, Contact):
|
||||||
assert contact.account == self
|
assert contact.account == self
|
||||||
contact_id = contact.id
|
contact_id = contact.id
|
||||||
@@ -284,7 +261,7 @@ class Account:
|
|||||||
return Contact(self, SpecialContactId.SELF)
|
return Contact(self, SpecialContactId.SELF)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_contact(self) -> Contact:
|
def device_contact(self) -> Chat:
|
||||||
"""Account's device contact."""
|
"""Account's device contact."""
|
||||||
return Contact(self, SpecialContactId.DEVICE)
|
return Contact(self, SpecialContactId.DEVICE)
|
||||||
|
|
||||||
@@ -372,7 +349,7 @@ class Account:
|
|||||||
return Chat(self, chat_id)
|
return Chat(self, chat_id)
|
||||||
|
|
||||||
def secure_join(self, qrdata: str) -> Chat:
|
def secure_join(self, qrdata: str) -> Chat:
|
||||||
"""Continue the SecureJoin protocol started on another device.
|
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on another device.
|
||||||
|
|
||||||
The function returns immediately and the handshake runs in background, sending
|
The function returns immediately and the handshake runs in background, sending
|
||||||
and receiving several messages.
|
and receiving several messages.
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ class Chat:
|
|||||||
def get_contacts(self) -> list[Contact]:
|
def get_contacts(self) -> list[Contact]:
|
||||||
"""Get the contacts belonging to this chat.
|
"""Get the contacts belonging to this chat.
|
||||||
|
|
||||||
For single chats self-address is not included.
|
For single/direct chats self-address is not included.
|
||||||
"""
|
"""
|
||||||
contacts = 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]
|
return [Contact(self.account, contact_id) for contact_id in contacts]
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class EventType(str, Enum):
|
|||||||
IMAP_CONNECTED = "ImapConnected"
|
IMAP_CONNECTED = "ImapConnected"
|
||||||
SMTP_MESSAGE_SENT = "SmtpMessageSent"
|
SMTP_MESSAGE_SENT = "SmtpMessageSent"
|
||||||
IMAP_MESSAGE_DELETED = "ImapMessageDeleted"
|
IMAP_MESSAGE_DELETED = "ImapMessageDeleted"
|
||||||
|
IMAP_MESSAGE_MOVED = "ImapMessageMoved"
|
||||||
IMAP_INBOX_IDLE = "ImapInboxIdle"
|
IMAP_INBOX_IDLE = "ImapInboxIdle"
|
||||||
NEW_BLOB_FILE = "NewBlobFile"
|
NEW_BLOB_FILE = "NewBlobFile"
|
||||||
DELETED_BLOB_FILE = "DeletedBlobFile"
|
DELETED_BLOB_FILE = "DeletedBlobFile"
|
||||||
@@ -53,7 +54,6 @@ class EventType(str, Enum):
|
|||||||
MSG_DELIVERED = "MsgDelivered"
|
MSG_DELIVERED = "MsgDelivered"
|
||||||
MSG_FAILED = "MsgFailed"
|
MSG_FAILED = "MsgFailed"
|
||||||
MSG_READ = "MsgRead"
|
MSG_READ = "MsgRead"
|
||||||
MSG_READ_COUNT_CHANGED = "MsgReadCountChanged"
|
|
||||||
MSG_DELETED = "MsgDeleted"
|
MSG_DELETED = "MsgDeleted"
|
||||||
CHAT_MODIFIED = "ChatModified"
|
CHAT_MODIFIED = "ChatModified"
|
||||||
CHAT_DELETED = "ChatDeleted"
|
CHAT_DELETED = "ChatDeleted"
|
||||||
@@ -69,7 +69,6 @@ class EventType(str, Enum):
|
|||||||
SELFAVATAR_CHANGED = "SelfavatarChanged"
|
SELFAVATAR_CHANGED = "SelfavatarChanged"
|
||||||
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
|
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
|
||||||
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
||||||
ACCOUNTS_BACKGROUND_FETCH_DONE = "AccountsBackgroundFetchDone"
|
|
||||||
CHATLIST_CHANGED = "ChatlistChanged"
|
CHATLIST_CHANGED = "ChatlistChanged"
|
||||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||||
ACCOUNTS_CHANGED = "AccountsChanged"
|
ACCOUNTS_CHANGED = "AccountsChanged"
|
||||||
@@ -97,7 +96,7 @@ class ChatType(str, Enum):
|
|||||||
"""Chat type."""
|
"""Chat type."""
|
||||||
|
|
||||||
SINGLE = "Single"
|
SINGLE = "Single"
|
||||||
"""Single chat (a chat with a with a single contact)"""
|
"""1:1 chat, i.e. a direct chat with a single contact"""
|
||||||
|
|
||||||
GROUP = "Group"
|
GROUP = "Group"
|
||||||
|
|
||||||
@@ -231,6 +230,14 @@ class KeyGenType(IntEnum):
|
|||||||
RSA4096 = 3
|
RSA4096 = 3
|
||||||
|
|
||||||
|
|
||||||
|
# "Lp" means "login parameters"
|
||||||
|
class LpAuthFlag(IntEnum):
|
||||||
|
"""Authorization flags."""
|
||||||
|
|
||||||
|
OAUTH2 = 0x2
|
||||||
|
NORMAL = 0x4
|
||||||
|
|
||||||
|
|
||||||
class MediaQuality(IntEnum):
|
class MediaQuality(IntEnum):
|
||||||
"""Media quality setting."""
|
"""Media quality setting."""
|
||||||
|
|
||||||
@@ -246,6 +253,14 @@ class ProviderStatus(IntEnum):
|
|||||||
BROKEN = 3
|
BROKEN = 3
|
||||||
|
|
||||||
|
|
||||||
|
class PushNotifyState(IntEnum):
|
||||||
|
"""Push notifications state."""
|
||||||
|
|
||||||
|
NOT_CONNECTED = 0
|
||||||
|
HEARTBEAT = 1
|
||||||
|
CONNECTED = 2
|
||||||
|
|
||||||
|
|
||||||
class ShowEmails(IntEnum):
|
class ShowEmails(IntEnum):
|
||||||
"""Show emails mode."""
|
"""Show emails mode."""
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class Contact:
|
|||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
def create_chat(self) -> "Chat":
|
def create_chat(self) -> "Chat":
|
||||||
"""Create or get an existing single chat for this contact."""
|
"""Create or get an existing 1:1 chat for this contact."""
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
|
|
||||||
return Chat(
|
return Chat(
|
||||||
|
|||||||
@@ -48,13 +48,6 @@ class DeltaChat:
|
|||||||
"""Stop ongoing background fetch."""
|
"""Stop ongoing background fetch."""
|
||||||
self.rpc.stop_background_fetch()
|
self.rpc.stop_background_fetch()
|
||||||
|
|
||||||
def wait_for_event(self, event_type=None) -> AttrDict:
|
|
||||||
"""Wait until the next account manager event and return it."""
|
|
||||||
while True:
|
|
||||||
next_event = AttrDict(self.rpc.wait_for_event(0))
|
|
||||||
if event_type is None or next_event.kind == event_type:
|
|
||||||
return next_event
|
|
||||||
|
|
||||||
def maybe_network(self) -> None:
|
def maybe_network(self) -> None:
|
||||||
"""Indicate that the network conditions might have changed."""
|
"""Indicate that the network conditions might have changed."""
|
||||||
self.rpc.maybe_network()
|
self.rpc.maybe_network()
|
||||||
@@ -74,7 +67,3 @@ class DeltaChat:
|
|||||||
def stop_sending_locations(self) -> None:
|
def stop_sending_locations(self) -> None:
|
||||||
"""Stop sending locations to all chats."""
|
"""Stop sending locations to all chats."""
|
||||||
return self.rpc.stop_sending_locations()
|
return self.rpc.stop_sending_locations()
|
||||||
|
|
||||||
def is_sending_finished(self) -> bool:
|
|
||||||
"""Return true if sending queues of all accounts are empty."""
|
|
||||||
return self.rpc.is_sending_finished()
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ class Message:
|
|||||||
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
|
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
|
||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
def send_webxdc_realtime_data(self, data):
|
def send_webxdc_realtime_data(self, data) -> None:
|
||||||
"""Send data to the realtime channel."""
|
"""Send data to the realtime channel."""
|
||||||
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ import os
|
|||||||
import pathlib
|
import pathlib
|
||||||
import platform
|
import platform
|
||||||
import random
|
import random
|
||||||
import socket
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
from typing import AsyncGenerator, Optional
|
||||||
import urllib.parse
|
|
||||||
from typing import Iterator, Optional
|
|
||||||
|
|
||||||
|
import execnet
|
||||||
|
import py
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
|
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
|
||||||
@@ -27,42 +26,19 @@ Currently this is "Messages are end-to-end encrypted."
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
|
||||||
# Run only in the xdist controller, before the workers exist.
|
|
||||||
if not hasattr(config, "workerinput"):
|
|
||||||
domain = os.environ.get("CHATMAIL_DOMAIN")
|
|
||||||
if domain:
|
|
||||||
check_chatmail_domain_and_warmup_dns_cache(domain)
|
|
||||||
|
|
||||||
|
|
||||||
def check_chatmail_domain_and_warmup_dns_cache(domain):
|
|
||||||
for i in range(6):
|
|
||||||
try:
|
|
||||||
socket.getaddrinfo(domain, 443)
|
|
||||||
return
|
|
||||||
except socket.gaierror as e:
|
|
||||||
error = e
|
|
||||||
logging.warning(f"DNS resolution of {domain} failed (attempt {i}): {e}")
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
pytest.exit(f"cannot resolve chatmail relay domain {domain}: {error}")
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_report_header():
|
def pytest_report_header():
|
||||||
headers = [f"CHATMAIL_DOMAIN: {os.environ.get('CHATMAIL_DOMAIN')}"]
|
|
||||||
for base in os.get_exec_path():
|
for base in os.get_exec_path():
|
||||||
fn = pathlib.Path(base).joinpath(base, "deltachat-rpc-server")
|
fn = pathlib.Path(base).joinpath(base, "deltachat-rpc-server")
|
||||||
if fn.exists():
|
if fn.exists():
|
||||||
proc = subprocess.Popen([str(fn), "--version"], stderr=subprocess.PIPE)
|
proc = subprocess.Popen([str(fn), "--version"], stderr=subprocess.PIPE)
|
||||||
proc.wait()
|
proc.wait()
|
||||||
version = proc.stderr.read().decode().strip()
|
version = proc.stderr.read().decode().strip()
|
||||||
headers.append(f"RPC-SERVER: {fn} [{version}]")
|
return f"deltachat-rpc-server: {fn} [{version}]"
|
||||||
break
|
|
||||||
|
|
||||||
return headers
|
return None
|
||||||
|
|
||||||
|
|
||||||
class RPCAccountFactory:
|
class ACFactory:
|
||||||
"""Test account factory."""
|
"""Test account factory."""
|
||||||
|
|
||||||
def __init__(self, deltachat: DeltaChat) -> None:
|
def __init__(self, deltachat: DeltaChat) -> None:
|
||||||
@@ -76,7 +52,7 @@ class RPCAccountFactory:
|
|||||||
"""Create a new unconfigured bot."""
|
"""Create a new unconfigured bot."""
|
||||||
return Bot(self.get_unconfigured_account())
|
return Bot(self.get_unconfigured_account())
|
||||||
|
|
||||||
def get_credentials(self) -> tuple[str, str]:
|
def get_credentials(self) -> (str, str):
|
||||||
"""Generate new credentials for chatmail account."""
|
"""Generate new credentials for chatmail account."""
|
||||||
domain = os.environ["CHATMAIL_DOMAIN"]
|
domain = os.environ["CHATMAIL_DOMAIN"]
|
||||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||||
@@ -128,7 +104,7 @@ class RPCAccountFactory:
|
|||||||
return ac_clone
|
return ac_clone
|
||||||
|
|
||||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||||
"""Create a new single chat between ac1 and ac2 accepted on both sides.
|
"""Create a new 1:1 chat between ac1 and ac2 accepted on both sides.
|
||||||
|
|
||||||
Returned chat is a chat with ac2 from ac1 point of view.
|
Returned chat is a chat with ac2 from ac1 point of view.
|
||||||
"""
|
"""
|
||||||
@@ -175,7 +151,7 @@ class RPCAccountFactory:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def rpc(tmp_path) -> Iterator[Rpc]:
|
def rpc(tmp_path) -> AsyncGenerator:
|
||||||
"""RPC client fixture."""
|
"""RPC client fixture."""
|
||||||
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
||||||
with rpc_server:
|
with rpc_server:
|
||||||
@@ -189,20 +165,20 @@ def dc(rpc) -> DeltaChat:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def acf(dc) -> RPCAccountFactory:
|
def acfactory(dc) -> AsyncGenerator:
|
||||||
"""Return account factory fixture."""
|
"""Return account factory fixture."""
|
||||||
return RPCAccountFactory(dc)
|
return ACFactory(dc)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def rpcdata():
|
def data():
|
||||||
"""Test data."""
|
"""Test data."""
|
||||||
|
|
||||||
class Data:
|
class Data:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
for path in pathlib.Path(__file__).parents:
|
for path in reversed(py.path.local(__file__).parts()):
|
||||||
datadir = path / "test-data"
|
datadir = path.join("test-data")
|
||||||
if datadir.is_dir():
|
if datadir.isdir():
|
||||||
self.path = datadir
|
self.path = datadir
|
||||||
return
|
return
|
||||||
raise Exception("Data path cannot be found")
|
raise Exception("Data path cannot be found")
|
||||||
@@ -292,33 +268,26 @@ def get_core_python_env(tmp_path_factory):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def alice_and_remote_bob(tmp_path, acf, get_core_python_env):
|
def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
|
||||||
"""return local Alice account, a contact to bob, and a remote 'eval' function for bob.
|
"""return local Alice account, a contact to bob, and a remote 'eval' function for bob.
|
||||||
|
|
||||||
The 'eval' function allows to remote-execute arbitrary expressions
|
The 'eval' function allows to remote-execute arbitrary expressions
|
||||||
that can use the `bob` online account, and the `bob_contact_alice`.
|
that can use the `bob` online account, and the `bob_contact_alice`.
|
||||||
"""
|
"""
|
||||||
from execnet import makegateway
|
|
||||||
|
|
||||||
def factory(core_version):
|
def factory(core_version):
|
||||||
python, rpc_server_path = get_core_python_env(core_version)
|
python, rpc_server_path = get_core_python_env(core_version)
|
||||||
gw = makegateway(f"popen//python={python}")
|
gw = execnet.makegateway(f"popen//python={python}")
|
||||||
|
|
||||||
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
|
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
|
||||||
channel = gw.remote_exec(remote_bob_loop)
|
channel = gw.remote_exec(remote_bob_loop)
|
||||||
|
cm = os.environ.get("CHATMAIL_DOMAIN")
|
||||||
# old cores need "ic=3" to accept
|
|
||||||
# the self-signed cert of an underscore domain
|
|
||||||
addr, password = acf.get_credentials()
|
|
||||||
dclogin_qr = f"dclogin://{urllib.parse.quote(addr, safe='@')}?p={urllib.parse.quote(password)}&v=1"
|
|
||||||
if os.environ["CHATMAIL_DOMAIN"].startswith("_"):
|
|
||||||
dclogin_qr += "&ic=3"
|
|
||||||
|
|
||||||
# trigger getting an online account on bob's side
|
# trigger getting an online account on bob's side
|
||||||
channel.send((accounts_dir, str(rpc_server_path), dclogin_qr))
|
channel.send((accounts_dir, str(rpc_server_path), cm))
|
||||||
|
|
||||||
# meanwhile get a local alice account
|
# meanwhile get a local alice account
|
||||||
alice = acf.get_online_account()
|
alice = acfactory.get_online_account()
|
||||||
channel.send(alice.self_contact.make_vcard())
|
channel.send(alice.self_contact.make_vcard())
|
||||||
|
|
||||||
# wait for bob to have started
|
# wait for bob to have started
|
||||||
@@ -347,8 +316,10 @@ def remote_bob_loop(channel):
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from deltachat_rpc_client import DeltaChat, Rpc
|
from deltachat_rpc_client import DeltaChat, Rpc
|
||||||
|
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||||
|
|
||||||
accounts_dir, rpc_server_path, dclogin_qr = channel.receive()
|
accounts_dir, rpc_server_path, chatmail_domain = channel.receive()
|
||||||
|
os.environ["CHATMAIL_DOMAIN"] = chatmail_domain
|
||||||
|
|
||||||
# older core versions don't support specifying rpc_server_path
|
# older core versions don't support specifying rpc_server_path
|
||||||
# so we can't just pass `rpc_server_path` argument to Rpc constructor
|
# so we can't just pass `rpc_server_path` argument to Rpc constructor
|
||||||
@@ -359,13 +330,8 @@ def remote_bob_loop(channel):
|
|||||||
with rpc:
|
with rpc:
|
||||||
dc = DeltaChat(rpc)
|
dc = DeltaChat(rpc)
|
||||||
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
|
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
|
||||||
|
acfactory = ACFactory(dc)
|
||||||
# RPCAccountFactory would configure from a "dcaccount" QR,
|
bob = acfactory.get_online_account()
|
||||||
# which old cores cannot use on underscore domains
|
|
||||||
bob = dc.add_account()
|
|
||||||
bob.add_transport_from_qr(dclogin_qr)
|
|
||||||
bob.bring_online()
|
|
||||||
|
|
||||||
alice_vcard = channel.receive()
|
alice_vcard = channel.receive()
|
||||||
[alice_contact] = bob.import_vcard(alice_vcard)
|
[alice_contact] = bob.import_vcard(alice_vcard)
|
||||||
ns = {"bob": bob, "bob_contact_alice": alice_contact}
|
ns = {"bob": bob, "bob_contact_alice": alice_contact}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -39,15 +38,8 @@ class RpcMethod:
|
|||||||
"params": args,
|
"params": args,
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
}
|
}
|
||||||
queue: Queue = Queue()
|
self.rpc.request_results[request_id] = queue = Queue()
|
||||||
# Register before testing for shutdown, so that either the reader loop
|
self.rpc.request_queue.put(request)
|
||||||
# finds this request while draining, or the test below catches it here.
|
|
||||||
# Testing first would race with the reader loop finishing in between.
|
|
||||||
self.rpc.request_results[request_id] = queue
|
|
||||||
if self.rpc.request_queue_closed:
|
|
||||||
self.rpc._fail_request(request_id)
|
|
||||||
else:
|
|
||||||
self.rpc.request_queue.put(request)
|
|
||||||
|
|
||||||
def rpc_future():
|
def rpc_future():
|
||||||
"""Wait for the request to receive a result."""
|
"""Wait for the request to receive a result."""
|
||||||
@@ -86,10 +78,6 @@ class Rpc:
|
|||||||
# Map from request ID to a Queue which provides a single result
|
# Map from request ID to a Queue which provides a single result
|
||||||
self.request_results: dict[int, Queue]
|
self.request_results: dict[int, Queue]
|
||||||
self.request_queue: Queue[Any]
|
self.request_queue: Queue[Any]
|
||||||
# Emulates `request_queue.shutdown(immediate=False)`, which needs Python 3.13:
|
|
||||||
# https://github.com/python/cpython/blob/v3.13.0/Lib/queue.py#L236-L257
|
|
||||||
# Note that `request_queue_closed` is set by the reader loop.
|
|
||||||
self.request_queue_closed: bool
|
|
||||||
self.closing: bool
|
self.closing: bool
|
||||||
self.reader_thread: Thread
|
self.reader_thread: Thread
|
||||||
self.writer_thread: Thread
|
self.writer_thread: Thread
|
||||||
@@ -119,7 +107,6 @@ class Rpc:
|
|||||||
self.event_queues = {}
|
self.event_queues = {}
|
||||||
self.request_results = {}
|
self.request_results = {}
|
||||||
self.request_queue = Queue()
|
self.request_queue = Queue()
|
||||||
self.request_queue_closed = False
|
|
||||||
self.closing = False
|
self.closing = False
|
||||||
self.reader_thread = Thread(target=self.reader_loop)
|
self.reader_thread = Thread(target=self.reader_loop)
|
||||||
self.reader_thread.start()
|
self.reader_thread.start()
|
||||||
@@ -136,8 +123,6 @@ class Rpc:
|
|||||||
# The reader_loop already saw EOF on stdout, so the process
|
# The reader_loop already saw EOF on stdout, so the process
|
||||||
# has exited and stderr is available.
|
# has exited and stderr is available.
|
||||||
stderr = self.process.stderr.read().decode(errors="replace").strip()
|
stderr = self.process.stderr.read().decode(errors="replace").strip()
|
||||||
self.closing = True
|
|
||||||
self._shutdown_loops()
|
|
||||||
if stderr:
|
if stderr:
|
||||||
raise JsonRpcError(f"RPC server failed to start: {stderr}") from e
|
raise JsonRpcError(f"RPC server failed to start: {stderr}") from e
|
||||||
raise JsonRpcError(f"RPC server startup check failed: {e}") from e
|
raise JsonRpcError(f"RPC server startup check failed: {e}") from e
|
||||||
@@ -149,35 +134,12 @@ class Rpc:
|
|||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Terminate RPC server process and wait until the reader loop finishes."""
|
"""Terminate RPC server process and wait until the reader loop finishes."""
|
||||||
self.closing = True
|
self.closing = True
|
||||||
# JSON-RPC error may happen if RPC server process has crashed already.
|
self.stop_io_for_all_accounts()
|
||||||
# We still want to shutdown all threads in this case.
|
|
||||||
with contextlib.suppress(JsonRpcError):
|
|
||||||
self.stop_io_for_all_accounts()
|
|
||||||
# Let `events_loop` stop cleanly on `closing` before the pipe goes away,
|
|
||||||
# otherwise it might exit through an "RPC server closed" error instead.
|
|
||||||
self.events_thread.join()
|
self.events_thread.join()
|
||||||
self._shutdown_loops()
|
self.process.stdin.close()
|
||||||
|
|
||||||
def _shutdown_loops(self) -> None:
|
|
||||||
"""Close the server pipe and wait for the loop threads to finish.
|
|
||||||
|
|
||||||
The writer blocks on an empty request queue,
|
|
||||||
so it needs the sentinel to notice the shutdown.
|
|
||||||
"""
|
|
||||||
with contextlib.suppress(BrokenPipeError):
|
|
||||||
# An exited server may leave data unflushed,
|
|
||||||
# which close() would try to write out again.
|
|
||||||
self.process.stdin.close()
|
|
||||||
self.request_queue.put(None)
|
|
||||||
self.reader_thread.join()
|
self.reader_thread.join()
|
||||||
|
self.request_queue.put(None)
|
||||||
self.writer_thread.join()
|
self.writer_thread.join()
|
||||||
self.events_thread.join()
|
|
||||||
|
|
||||||
def _fail_request(self, request_id: int) -> None:
|
|
||||||
"""Answer a registered request with an error, unless it was answered already."""
|
|
||||||
queue = self.request_results.pop(request_id, None)
|
|
||||||
if queue is not None:
|
|
||||||
queue.put({"error": {"code": -32000, "message": "RPC server closed"}})
|
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.start()
|
self.start()
|
||||||
@@ -200,11 +162,9 @@ class Rpc:
|
|||||||
# Log an exception if the reader loop dies.
|
# Log an exception if the reader loop dies.
|
||||||
logging.exception("Exception in the reader loop")
|
logging.exception("Exception in the reader loop")
|
||||||
finally:
|
finally:
|
||||||
# Shut the request queue first, so that requests registered from now
|
# Unblock any pending requests when the server closes stdout.
|
||||||
# on are failed by their caller, then answer the pending ones here.
|
for _request_id, queue in self.request_results.items():
|
||||||
self.request_queue_closed = True
|
queue.put({"error": {"code": -32000, "message": "RPC server closed"}})
|
||||||
for request_id in list(self.request_results):
|
|
||||||
self._fail_request(request_id)
|
|
||||||
|
|
||||||
def writer_loop(self) -> None:
|
def writer_loop(self) -> None:
|
||||||
"""Writer loop ensuring only a single thread writes requests."""
|
"""Writer loop ensuring only a single thread writes requests."""
|
||||||
|
|||||||
@@ -22,18 +22,8 @@ ALL = "1:*"
|
|||||||
class DirectImap:
|
class DirectImap:
|
||||||
"""Internal Python-level IMAP handling."""
|
"""Internal Python-level IMAP handling."""
|
||||||
|
|
||||||
def __init__(self, account: Account, addr=None, password=None) -> None:
|
def __init__(self, account: Account) -> None:
|
||||||
self.account = account
|
self.account = account
|
||||||
if addr is None or password is None:
|
|
||||||
transport = account.list_transports()[-1]
|
|
||||||
if addr is None:
|
|
||||||
self.addr = transport["addr"]
|
|
||||||
else:
|
|
||||||
self.addr = addr
|
|
||||||
if password is None:
|
|
||||||
self.password = transport["password"]
|
|
||||||
else:
|
|
||||||
self.password = password
|
|
||||||
self.logid = account.get_config("displayname") or id(account)
|
self.logid = account.get_config("displayname") or id(account)
|
||||||
self._idling = False
|
self._idling = False
|
||||||
self.connect()
|
self.connect()
|
||||||
@@ -43,15 +33,11 @@ class DirectImap:
|
|||||||
host = self.account.get_config("configured_mail_server")
|
host = self.account.get_config("configured_mail_server")
|
||||||
port = 993
|
port = 993
|
||||||
|
|
||||||
user = self.addr
|
user = self.account.get_config("addr")
|
||||||
host = user.rsplit("@")[-1]
|
host = user.rsplit("@")[-1]
|
||||||
pw = self.password
|
pw = self.account.get_config("mail_pw")
|
||||||
|
|
||||||
ssl_context = ssl.create_default_context()
|
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
|
||||||
if host.startswith("_"):
|
|
||||||
ssl_context.check_hostname = False
|
|
||||||
ssl_context.verify_mode = ssl.CERT_NONE
|
|
||||||
self.conn = MailBox(host, port, ssl_context=ssl_context)
|
|
||||||
self.conn.login(user, pw)
|
self.conn.login(user, pw)
|
||||||
|
|
||||||
self.select_folder("INBOX")
|
self.select_folder("INBOX")
|
||||||
@@ -179,7 +165,7 @@ class DirectImap:
|
|||||||
self.conn.append(bytes(msg, encoding="ascii"), folder)
|
self.conn.append(bytes(msg, encoding="ascii"), folder)
|
||||||
|
|
||||||
def get_uid_by_message_id(self, message_id) -> str:
|
def get_uid_by_message_id(self, message_id) -> str:
|
||||||
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)), mark_seen=False)]
|
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
|
||||||
if len(msgs) == 0:
|
if len(msgs) == 0:
|
||||||
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||||
return msgs[0]
|
return msgs[0]
|
||||||
@@ -188,6 +174,9 @@ class DirectImap:
|
|||||||
class IdleManager:
|
class IdleManager:
|
||||||
def __init__(self, direct_imap) -> None:
|
def __init__(self, direct_imap) -> None:
|
||||||
self.direct_imap = direct_imap
|
self.direct_imap = direct_imap
|
||||||
|
# fetch latest messages before starting idle so that it only
|
||||||
|
# returns messages that arrive anew
|
||||||
|
self.direct_imap.conn.fetch("1:*")
|
||||||
self.direct_imap.conn.idle.start()
|
self.direct_imap.conn.idle.start()
|
||||||
|
|
||||||
def check(self, timeout=None) -> list[bytes]:
|
def check(self, timeout=None) -> list[bytes]:
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ from typing import TYPE_CHECKING
|
|||||||
from deltachat_rpc_client import EventType
|
from deltachat_rpc_client import EventType
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from deltachat_rpc_client.pytestplugin import RPCAccountFactory
|
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||||
|
|
||||||
|
|
||||||
def test_event_on_configuration(acf: RPCAccountFactory) -> None:
|
def test_event_on_configuration(acfactory: ACFactory) -> None:
|
||||||
"""
|
"""
|
||||||
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
|
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
|
||||||
"""
|
"""
|
||||||
|
|
||||||
addr, password = acf.get_credentials()
|
addr, password = acfactory.get_credentials()
|
||||||
account = acf.get_unconfigured_account()
|
account = acfactory.get_unconfigured_account()
|
||||||
account.clear_all_events()
|
account.clear_all_events()
|
||||||
assert not account.is_configured()
|
assert not account.is_configured()
|
||||||
future = account.add_or_update_transport.future({"addr": addr, "password": password})
|
future = account.add_or_update_transport.future({"addr": addr, "password": password})
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from deltachat_rpc_client import EventType, Message
|
from deltachat_rpc_client import EventType, Message
|
||||||
|
|
||||||
|
|
||||||
def test_calls(acf) -> None:
|
def test_calls(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
place_call_info = "offer"
|
place_call_info = "offer"
|
||||||
accept_call_info = "answer"
|
accept_call_info = "answer"
|
||||||
@@ -35,14 +35,14 @@ def test_calls(acf) -> None:
|
|||||||
assert incoming_call_message.get_call_info().state.kind == "Completed"
|
assert incoming_call_message.get_call_info().state.kind == "Completed"
|
||||||
|
|
||||||
|
|
||||||
def test_video_call(acf) -> None:
|
def test_video_call(acfactory) -> None:
|
||||||
# Example from <https://datatracker.ietf.org/doc/rfc9143/>
|
# Example from <https://datatracker.ietf.org/doc/rfc9143/>
|
||||||
# with `s= ` replaced with `s=-`.
|
# with `s= ` replaced with `s=-`.
|
||||||
#
|
#
|
||||||
# `s=` cannot be empty according to RFC 3264,
|
# `s=` cannot be empty according to RFC 3264,
|
||||||
# so it is more clear as `s=-`.
|
# so it is more clear as `s=-`.
|
||||||
|
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
@@ -57,8 +57,8 @@ def test_video_call(acf) -> None:
|
|||||||
assert incoming_call_message.get_call_info().has_video
|
assert incoming_call_message.get_call_info().has_video
|
||||||
|
|
||||||
|
|
||||||
def test_audio_call(acf) -> None:
|
def test_audio_call(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
@@ -73,15 +73,15 @@ def test_audio_call(acf) -> None:
|
|||||||
assert not incoming_call_message.get_call_info().has_video
|
assert not incoming_call_message.get_call_info().has_video
|
||||||
|
|
||||||
|
|
||||||
def test_ice_servers(acf) -> None:
|
def test_ice_servers(acfactory) -> None:
|
||||||
alice = acf.get_online_account()
|
alice = acfactory.get_online_account()
|
||||||
|
|
||||||
ice_servers = alice.ice_servers()
|
ice_servers = alice.ice_servers()
|
||||||
assert len(ice_servers) == 1
|
assert len(ice_servers) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_no_contact_request_call(acf) -> None:
|
def test_no_contact_request_call(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||||
@@ -101,8 +101,8 @@ def test_no_contact_request_call(acf) -> None:
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def test_who_can_call_me_nobody(acf) -> None:
|
def test_who_can_call_me_nobody(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
# Bob sets "who can call me" to "nobody" (2)
|
# Bob sets "who can call me" to "nobody" (2)
|
||||||
bob.set_config("who_can_call_me", "2")
|
bob.set_config("who_can_call_me", "2")
|
||||||
@@ -128,9 +128,9 @@ def test_who_can_call_me_nobody(acf) -> None:
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def test_who_can_call_me_everybody(acf) -> None:
|
def test_who_can_call_me_everybody(acfactory) -> None:
|
||||||
"""Test that if "who can call me" setting is set to "everybody", calls arrive even in contact request chats."""
|
"""Test that if "who can call me" setting is set to "everybody", calls arrive even in contact request chats."""
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
# Bob sets "who can call me" to "nobody" (0)
|
# Bob sets "who can call me" to "nobody" (0)
|
||||||
bob.set_config("who_can_call_me", "0")
|
bob.set_config("who_can_call_me", "0")
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from deltachat_rpc_client import EventType, const
|
from deltachat_rpc_client import Account, EventType, const
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from deltachat_rpc_client.pytestplugin import RPCAccountFactory
|
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||||
|
|
||||||
|
|
||||||
def wait_for_chatlist_and_specific_item(account, chat_id):
|
def wait_for_chatlist_and_specific_item(account, chat_id):
|
||||||
@@ -40,11 +40,11 @@ def wait_for_chatlist(account):
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def test_delivery_status(acf: RPCAccountFactory) -> None:
|
def test_delivery_status(acfactory: ACFactory) -> None:
|
||||||
"""
|
"""
|
||||||
Test change status on chatlistitem when status changes (delivered, read)
|
Test change status on chatlistitem when status changes (delivered, read)
|
||||||
"""
|
"""
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
@@ -82,11 +82,11 @@ def test_delivery_status(acf: RPCAccountFactory) -> None:
|
|||||||
assert chat_item["summaryStatus"] == const.MessageState.OUT_MDN_RCVD
|
assert chat_item["summaryStatus"] == const.MessageState.OUT_MDN_RCVD
|
||||||
|
|
||||||
|
|
||||||
def test_delivery_status_failed(acf: RPCAccountFactory) -> None:
|
def test_delivery_status_failed(acfactory: ACFactory) -> None:
|
||||||
"""
|
"""
|
||||||
Test change status on chatlistitem when status changes failed
|
Test change status on chatlistitem when status changes failed
|
||||||
"""
|
"""
|
||||||
(alice,) = acf.get_online_accounts(1)
|
(alice,) = acfactory.get_online_accounts(1)
|
||||||
alice.set_config("force_encryption", "0")
|
alice.set_config("force_encryption", "0")
|
||||||
|
|
||||||
invalid_contact = alice.create_contact("example@example.com", "invalid address")
|
invalid_contact = alice.create_contact("example@example.com", "invalid address")
|
||||||
@@ -110,12 +110,12 @@ def test_delivery_status_failed(acf: RPCAccountFactory) -> None:
|
|||||||
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
|
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
|
||||||
|
|
||||||
|
|
||||||
def test_download_on_demand(acf: RPCAccountFactory, rpcdata) -> None:
|
def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||||
"""
|
"""
|
||||||
Test if download on demand emits chatlist update events.
|
Test if download on demand emits chatlist update events.
|
||||||
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
|
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
|
||||||
"""
|
"""
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
@@ -128,7 +128,7 @@ def test_download_on_demand(acf: RPCAccountFactory, rpcdata) -> None:
|
|||||||
msg.get_snapshot().chat.accept()
|
msg.get_snapshot().chat.accept()
|
||||||
bob.get_chat_by_id(chat_id).send_message(
|
bob.get_chat_by_id(chat_id).send_message(
|
||||||
"Hello World, this message is bigger than 5 bytes",
|
"Hello World, this message is bigger than 5 bytes",
|
||||||
file=rpcdata.get_path("image/screenshot.jpg"),
|
file="../test-data/image/screenshot.jpg",
|
||||||
)
|
)
|
||||||
|
|
||||||
message = alice.wait_for_incoming_msg()
|
message = alice.wait_for_incoming_msg()
|
||||||
@@ -144,8 +144,8 @@ def test_download_on_demand(acf: RPCAccountFactory, rpcdata) -> None:
|
|||||||
wait_for_chatlist_specific_item(alice, chat_id)
|
wait_for_chatlist_specific_item(alice, chat_id)
|
||||||
|
|
||||||
|
|
||||||
def get_multi_account_test_setup(acf: RPCAccountFactory) -> list:
|
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
@@ -161,12 +161,12 @@ def get_multi_account_test_setup(acf: RPCAccountFactory) -> list:
|
|||||||
return [alice, alice_second_device, bob, alice_chat_bob]
|
return [alice, alice_second_device, bob, alice_chat_bob]
|
||||||
|
|
||||||
|
|
||||||
def test_imap_sync_seen_msgs(acf: RPCAccountFactory) -> None:
|
def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||||
"""
|
"""
|
||||||
Test that chatlist changed events are emitted for the second device
|
Test that chatlist changed events are emitted for the second device
|
||||||
when the message is marked as read on the first device
|
when the message is marked as read on the first device
|
||||||
"""
|
"""
|
||||||
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acf)
|
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||||
|
|
||||||
bob.create_chat(alice)
|
bob.create_chat(alice)
|
||||||
|
|
||||||
@@ -191,11 +191,11 @@ def test_imap_sync_seen_msgs(acf: RPCAccountFactory) -> None:
|
|||||||
wait_for_chatlist_specific_item(alice, alice_chat_bob.id)
|
wait_for_chatlist_specific_item(alice, alice_chat_bob.id)
|
||||||
|
|
||||||
|
|
||||||
def test_multidevice_sync_chat(acf: RPCAccountFactory) -> None:
|
def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
|
||||||
"""
|
"""
|
||||||
Test multidevice sync: syncing chat visibility and muting across multiple devices
|
Test multidevice sync: syncing chat visibility and muting across multiple devices
|
||||||
"""
|
"""
|
||||||
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acf)
|
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||||
|
|
||||||
alice_chat_bob.archive()
|
alice_chat_bob.archive()
|
||||||
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
import time
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -17,7 +16,7 @@ def test_install_venv_and_use_other_core(tmp_path, get_core_python_env):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("version", ["2.24.0"])
|
@pytest.mark.parametrize("version", ["2.24.0"])
|
||||||
def test_qr_setup_contact(acf, alice_and_remote_bob, version) -> None:
|
def test_qr_setup_contact(acfactory, alice_and_remote_bob, version) -> None:
|
||||||
"""Test other-core Bob profile can do securejoin with Alice on current core."""
|
"""Test other-core Bob profile can do securejoin with Alice on current core."""
|
||||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
|
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
|
||||||
|
|
||||||
@@ -25,48 +24,28 @@ def test_qr_setup_contact(acf, alice_and_remote_bob, version) -> None:
|
|||||||
remote_eval(f"bob.secure_join({qr_code!r})")
|
remote_eval(f"bob.secure_join({qr_code!r})")
|
||||||
alice.wait_for_securejoin_inviter_success()
|
alice.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
# Test that Alice verified Bob's profile.
|
||||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||||
assert alice_contact_bob_snapshot.e2ee_avail
|
assert alice_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
||||||
|
|
||||||
# The old core still marks Alice as verified, so the handshake is unchanged on the wire.
|
# Test that Bob verified Alice's profile.
|
||||||
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
|
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
|
||||||
|
|
||||||
# Test that Bob can also scan a QR code
|
# Test that Bob can also scan a QR code
|
||||||
# of Alice for which the key is not known yet.
|
# of Alice for which the key is not known yet.
|
||||||
# For the test above Bob already knew the key from a vCard.
|
# For the test above Bob already knew the key from a vCard.
|
||||||
alice2 = acf.get_online_account()
|
alice2 = acfactory.get_online_account()
|
||||||
qr_code = alice2.get_qr_code()
|
qr_code = alice2.get_qr_code()
|
||||||
remote_eval(f"bob.secure_join({qr_code!r})")
|
remote_eval(f"bob.secure_join({qr_code!r})")
|
||||||
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
||||||
alice2.wait_for_securejoin_inviter_success()
|
alice2.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("version", ["2.24.0"])
|
|
||||||
def test_qr_setup_contact_multitransport(acf, alice_and_remote_bob, version) -> None:
|
|
||||||
"""Test other-core Bob profile can do securejoin with Alice on current core, with multiple transports."""
|
|
||||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
|
|
||||||
relay_qr = acf.get_account_qr()
|
|
||||||
alice.add_transport_from_qr(relay_qr)
|
|
||||||
alice.add_transport_from_qr(relay_qr)
|
|
||||||
|
|
||||||
qr_code = alice.get_qr_code()
|
|
||||||
remote_eval(f"bob.secure_join({qr_code!r})")
|
|
||||||
alice.wait_for_securejoin_inviter_success()
|
|
||||||
|
|
||||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
|
||||||
assert alice_contact_bob_snapshot.e2ee_avail
|
|
||||||
|
|
||||||
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
|
||||||
|
|
||||||
# The old core still marks Alice as verified, so the handshake is unchanged on the wire.
|
|
||||||
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
|
|
||||||
|
|
||||||
|
|
||||||
def test_send_and_receive_message(alice_and_remote_bob) -> None:
|
def test_send_and_receive_message(alice_and_remote_bob) -> None:
|
||||||
"""Test other-core Bob profile can send a message to Alice on current core."""
|
"""Test other-core Bob profile can send a message to Alice on current core."""
|
||||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.23.0")
|
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
|
||||||
|
|
||||||
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
|
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
|
||||||
|
|
||||||
@@ -74,149 +53,14 @@ def test_send_and_receive_message(alice_and_remote_bob) -> None:
|
|||||||
assert msg.get_snapshot().text == "hello"
|
assert msg.get_snapshot().text == "hello"
|
||||||
|
|
||||||
|
|
||||||
def test_second_device(acf, alice_and_remote_bob) -> None:
|
def test_second_device(acfactory, alice_and_remote_bob) -> None:
|
||||||
"""Test setting up current version as a second device for old version."""
|
"""Test setting up current version as a second device for old version."""
|
||||||
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.23.0")
|
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
|
||||||
|
|
||||||
remote_eval("locals().setdefault('future', bob._rpc.provide_backup.future(bob.id))")
|
remote_eval("locals().setdefault('future', bob._rpc.provide_backup.future(bob.id))")
|
||||||
qr = remote_eval("bob._rpc.get_backup_qr(bob.id)")
|
qr = remote_eval("bob._rpc.get_backup_qr(bob.id)")
|
||||||
new_account = acf.get_unconfigured_account()
|
new_account = acfactory.get_unconfigured_account()
|
||||||
new_account._rpc.get_backup(new_account.id, qr)
|
new_account._rpc.get_backup(new_account.id, qr)
|
||||||
remote_eval("locals()['future']()")
|
remote_eval("locals()['future']()")
|
||||||
|
|
||||||
assert new_account.get_config("addr") == remote_eval("bob.get_config('addr')")
|
assert new_account.get_config("addr") == remote_eval("bob.get_config('addr')")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("replace_relay", [False, True], ids=["add", "replace"])
|
|
||||||
def test_keyupdate_against_core_2_48_march_2026(acf, alice_and_remote_bob, replace_relay):
|
|
||||||
"""Test 2.48 Bob learns a relay change of Alice from a keyupdate, and is shown nothing."""
|
|
||||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.48.0")
|
|
||||||
|
|
||||||
def bob_sees():
|
|
||||||
return remote_eval(
|
|
||||||
"{'chats': len(bob.get_chatlist()),"
|
|
||||||
" 'fresh': len(bob._rpc.get_fresh_msgs(bob.id)),"
|
|
||||||
" 'contacts': len(bob.get_contacts()),"
|
|
||||||
" 'alice_chat': bob._rpc.get_chat_id_by_contact_id(bob.id, bob_contact_alice.id) or 0}",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Keyupdates go to contacts who plausibly hold our key:
|
|
||||||
# an accepted chat alone is not enough, a message must have flowed.
|
|
||||||
alice_chat = alice_contact_bob.create_chat()
|
|
||||||
alice.set_config("keyupdate_debounce", "1")
|
|
||||||
old_addr = alice.get_config("configured_addr")
|
|
||||||
alice_chat.send_text("hi")
|
|
||||||
assert remote_eval("bob.wait_for_incoming_msg().get_snapshot().text") == "hi"
|
|
||||||
before = bob_sees()
|
|
||||||
|
|
||||||
# Certificate merging keeps the newest direct key signature,
|
|
||||||
# and signature timestamps have one-second resolution:
|
|
||||||
# without waiting, the re-signed key can tie with the copy Bob holds, keeping his.
|
|
||||||
time.sleep(2)
|
|
||||||
alice.add_transport_from_qr(acf.get_account_qr())
|
|
||||||
(new_addr,) = [t["addr"] for t in alice.list_transports() if t["addr"] != old_addr]
|
|
||||||
if replace_relay:
|
|
||||||
alice.delete_transport(old_addr)
|
|
||||||
alice.bring_online()
|
|
||||||
|
|
||||||
# The 2.48 core has no encryption enforcement, but the keyupdate MDN without
|
|
||||||
# referenced message keeps it invisible; merging happens before the trashing.
|
|
||||||
for _ in range(60):
|
|
||||||
if new_addr in remote_eval("bob_contact_alice.get_encryption_info()"):
|
|
||||||
break
|
|
||||||
time.sleep(1)
|
|
||||||
else:
|
|
||||||
pytest.fail("Bob never received the keyupdate")
|
|
||||||
|
|
||||||
# It also leaves no trace: no chat with Alice, no message anywhere,
|
|
||||||
# and no address-contact for the address it was sent from.
|
|
||||||
assert bob_sees() == before
|
|
||||||
|
|
||||||
if replace_relay:
|
|
||||||
remote_eval("bob_contact_alice.create_chat().send_text('hello after replacement')")
|
|
||||||
assert alice.wait_for_incoming_msg().get_snapshot().text == "hello after replacement"
|
|
||||||
|
|
||||||
|
|
||||||
class LocalSide:
|
|
||||||
"""Alice on the core under test."""
|
|
||||||
|
|
||||||
def __init__(self, account, peer_contact):
|
|
||||||
self.account = account
|
|
||||||
self.peer_contact = peer_contact
|
|
||||||
self.chat = None
|
|
||||||
|
|
||||||
def make_qr(self, invite):
|
|
||||||
if invite == "group":
|
|
||||||
self.chat = self.account.create_group("Group")
|
|
||||||
elif invite == "broadcast":
|
|
||||||
self.chat = self.account.create_broadcast("Channel")
|
|
||||||
return self.chat.get_qr_code() if self.chat else self.account.get_qr_code()
|
|
||||||
|
|
||||||
def join(self, qr):
|
|
||||||
self.account.secure_join(qr)
|
|
||||||
|
|
||||||
def wait_inviter(self):
|
|
||||||
self.account.wait_for_securejoin_inviter_success()
|
|
||||||
|
|
||||||
def wait_joiner(self):
|
|
||||||
self.account.wait_for_securejoin_joiner_success()
|
|
||||||
|
|
||||||
def send_text(self, text):
|
|
||||||
chat = self.chat or self.peer_contact.create_chat()
|
|
||||||
chat.send_text(text)
|
|
||||||
|
|
||||||
def next_text(self):
|
|
||||||
return self.account.wait_for_incoming_msg().get_snapshot().text
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteSide:
|
|
||||||
"""Bob on the other core, driven through remote_eval."""
|
|
||||||
|
|
||||||
def __init__(self, remote_eval):
|
|
||||||
self.remote_eval = remote_eval
|
|
||||||
self.chat = None
|
|
||||||
|
|
||||||
def make_qr(self, invite):
|
|
||||||
if invite == "contact":
|
|
||||||
return self.remote_eval("bob.get_qr_code()")
|
|
||||||
create = {"group": "bob.create_group('Group')", "broadcast": "bob.create_broadcast('Channel')"}[invite]
|
|
||||||
self.remote_eval(f"locals().update(chat={create})")
|
|
||||||
self.chat = "chat"
|
|
||||||
return self.remote_eval("chat.get_qr_code()")
|
|
||||||
|
|
||||||
def join(self, qr):
|
|
||||||
self.remote_eval(f"bob.secure_join({qr!r})")
|
|
||||||
|
|
||||||
def wait_inviter(self):
|
|
||||||
self.remote_eval("bob.wait_for_securejoin_inviter_success()")
|
|
||||||
|
|
||||||
def wait_joiner(self):
|
|
||||||
self.remote_eval("bob.wait_for_securejoin_joiner_success()")
|
|
||||||
|
|
||||||
def send_text(self, text):
|
|
||||||
chat = self.chat or "bob_contact_alice.create_chat()"
|
|
||||||
self.remote_eval(f"{chat}.send_text({text!r})")
|
|
||||||
|
|
||||||
def next_text(self):
|
|
||||||
return self.remote_eval("bob.wait_for_incoming_msg().get_snapshot().text")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("version", ["2.48.0"])
|
|
||||||
@pytest.mark.parametrize("invite", ["contact", "group", "broadcast"])
|
|
||||||
@pytest.mark.parametrize("remote_invites", [False, True], ids=["local-invites", "remote-invites"])
|
|
||||||
def test_securejoin_invite(alice_and_remote_bob, version, invite, remote_invites):
|
|
||||||
"""Every invite link type works with either core as the inviter."""
|
|
||||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
|
|
||||||
local = LocalSide(alice, alice_contact_bob)
|
|
||||||
remote = RemoteSide(remote_eval)
|
|
||||||
inviter, joiner = (remote, local) if remote_invites else (local, remote)
|
|
||||||
|
|
||||||
joiner.join(inviter.make_qr(invite))
|
|
||||||
joiner.wait_joiner()
|
|
||||||
inviter.wait_inviter()
|
|
||||||
# Group and broadcast joins add an info message first.
|
|
||||||
if invite != "contact":
|
|
||||||
joiner.next_text()
|
|
||||||
|
|
||||||
inviter.send_text("hello")
|
|
||||||
assert joiner.next_text() == "hello"
|
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ from imap_tools import AND, U
|
|||||||
from deltachat_rpc_client import EventType
|
from deltachat_rpc_client import EventType
|
||||||
|
|
||||||
|
|
||||||
def test_moved_markseen(acf, direct_imap, log):
|
def test_moved_markseen(acfactory, direct_imap, log):
|
||||||
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
||||||
ac1 = acf.get_online_account()
|
ac1 = acfactory.get_online_account()
|
||||||
|
|
||||||
addr, password = acf.get_credentials()
|
addr, password = acfactory.get_credentials()
|
||||||
ac2 = acf.get_unconfigured_account()
|
ac2 = acfactory.get_unconfigured_account()
|
||||||
ac2.add_or_update_transport({"addr": addr, "password": password})
|
ac2.add_or_update_transport({"addr": addr, "password": password})
|
||||||
ac2.bring_online()
|
ac2.bring_online()
|
||||||
|
|
||||||
@@ -57,28 +57,24 @@ def test_moved_markseen(acf, direct_imap, log):
|
|||||||
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True, uid=U(1, "*")), mark_seen=False))) == 1
|
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True, uid=U(1, "*")), mark_seen=False))) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_markseen_message_and_mdn(acf, direct_imap):
|
def test_markseen_message_and_mdn(acfactory, direct_imap):
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
# Make sure that messages are not immediately auto-deleted on the server:
|
# Make sure that messages are not immediately auto-deleted on the server:
|
||||||
ac1.set_config("bcc_self", "1")
|
ac1.set_config("bcc_self", "1")
|
||||||
ac2.set_config("bcc_self", "1")
|
ac2.set_config("bcc_self", "1")
|
||||||
|
|
||||||
acf.get_accepted_chat(ac1, ac2).send_text("hi")
|
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
||||||
msg = ac2.wait_for_incoming_msg()
|
msg = ac2.wait_for_incoming_msg()
|
||||||
msg.mark_seen()
|
msg.mark_seen()
|
||||||
|
|
||||||
rex = re.compile("Marked messages ([0-9,:]+) in folder INBOX as seen.")
|
rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||||
|
|
||||||
# Each profile flags two messages but the logged UID set
|
|
||||||
# covers a varying number of them, so just count UIDs mentioned.
|
|
||||||
# We are not processing UID ranges, here we just care for two UIDs.
|
|
||||||
for ac in ac1, ac2:
|
for ac in ac1, ac2:
|
||||||
uids = set()
|
while True:
|
||||||
while len(uids) < 2:
|
|
||||||
event = ac.wait_for_event()
|
event = ac.wait_for_event()
|
||||||
if event.kind == EventType.INFO and (match := rex.search(event.msg)):
|
if event.kind == EventType.INFO and rex.search(event.msg):
|
||||||
uids.update(re.split("[,:]", match.group(1)))
|
break
|
||||||
|
|
||||||
ac1_direct_imap = direct_imap(ac1)
|
ac1_direct_imap = direct_imap(ac1)
|
||||||
ac2_direct_imap = direct_imap(ac2)
|
ac2_direct_imap = direct_imap(ac2)
|
||||||
@@ -91,8 +87,8 @@ def test_markseen_message_and_mdn(acf, direct_imap):
|
|||||||
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2
|
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_trash_multiple_messages(acf, direct_imap, log):
|
def test_trash_multiple_messages(acfactory, direct_imap, log):
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
ac2.stop_io()
|
ac2.stop_io()
|
||||||
|
|
||||||
# Make sure that messages are not immediately auto-deleted on the server:
|
# Make sure that messages are not immediately auto-deleted on the server:
|
||||||
@@ -101,7 +97,7 @@ def test_trash_multiple_messages(acf, direct_imap, log):
|
|||||||
ac2.set_config("sync_msgs", "0")
|
ac2.set_config("sync_msgs", "0")
|
||||||
|
|
||||||
ac2.start_io()
|
ac2.start_io()
|
||||||
chat12 = acf.get_accepted_chat(ac1, ac2)
|
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||||
|
|
||||||
log.section("ac1: sending 3 messages")
|
log.section("ac1: sending 3 messages")
|
||||||
texts = ["first", "second", "third"]
|
texts = ["first", "second", "third"]
|
||||||
|
|||||||
@@ -7,25 +7,15 @@ If you want to debug iroh at rust-trace/log level set
|
|||||||
RUST_LOG=iroh_net=trace,iroh_gossip=trace
|
RUST_LOG=iroh_net=trace,iroh_gossip=trace
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import itertools
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
from contextlib import contextmanager
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from deltachat_rpc_client import EventType
|
from deltachat_rpc_client import EventType
|
||||||
|
|
||||||
# Relays on underscore domains advertise themselves as iroh relay
|
|
||||||
# but serve a self-signed certificate that iroh's TLS stack rejects.
|
|
||||||
# Skipping instead of xfailing keeps the run fast:
|
|
||||||
# these tests only fail after waiting for realtime connections to time out.
|
|
||||||
pytestmark = pytest.mark.skipif(
|
|
||||||
os.environ.get("CHATMAIL_DOMAIN", "").startswith("_"),
|
|
||||||
reason="iroh does not accept the self-signed certificate of an underscore domain",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def path_to_webxdc(request):
|
def path_to_webxdc(request):
|
||||||
@@ -45,11 +35,7 @@ def log(msg):
|
|||||||
logging.info(msg)
|
logging.info(msg)
|
||||||
|
|
||||||
|
|
||||||
# payload used to probe/establish realtime connectivity, filtered out by tests
|
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
|
||||||
SETUP_DATA = b"realtime-setup"
|
|
||||||
|
|
||||||
|
|
||||||
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True):
|
|
||||||
assert ac1.get_config("webxdc_realtime_enabled") == "1"
|
assert ac1.get_config("webxdc_realtime_enabled") == "1"
|
||||||
assert ac2.get_config("webxdc_realtime_enabled") == "1"
|
assert ac2.get_config("webxdc_realtime_enabled") == "1"
|
||||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||||
@@ -66,44 +52,45 @@ def setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True):
|
|||||||
|
|
||||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||||
if wait:
|
|
||||||
wait_realtime_connected([(ac1_webxdc_msg, ac2_webxdc_msg)])
|
|
||||||
return ac1_webxdc_msg, ac2_webxdc_msg
|
return ac1_webxdc_msg, ac2_webxdc_msg
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
def setup_thread_send_realtime_data(msg, data):
|
||||||
def send_realtime_data_forever(msgs, data=None):
|
def thread_run():
|
||||||
stop = threading.Event()
|
for _i in range(10):
|
||||||
data = data or [SETUP_DATA] * len(msgs)
|
msg.send_webxdc_realtime_data(data)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
def thread_run(msg, payload):
|
threading.Thread(target=thread_run, daemon=True).start()
|
||||||
for i in itertools.count():
|
|
||||||
msg.send_webxdc_realtime_data(payload(i) if callable(payload) else payload)
|
|
||||||
if stop.wait(1):
|
|
||||||
return
|
|
||||||
|
|
||||||
for msg_payload in zip(msgs, data, strict=True):
|
|
||||||
threading.Thread(target=thread_run, args=msg_payload, daemon=True).start()
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
stop.set()
|
|
||||||
|
|
||||||
|
|
||||||
def wait_realtime_connected(msg_pairs):
|
def wait_receive_realtime_data(msg_data_list):
|
||||||
with send_realtime_data_forever([sender for sender, _ in msg_pairs]):
|
account = msg_data_list[0][0].account
|
||||||
for _, receiver in msg_pairs:
|
msg_data_list = msg_data_list[:]
|
||||||
receiver.account.wait_for_realtime_data(receiver.id)
|
|
||||||
|
log(f"account {account.id}: waiting for realtime data {msg_data_list}")
|
||||||
|
while msg_data_list:
|
||||||
|
event = account.wait_for_event()
|
||||||
|
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||||
|
for i, (msg, data) in enumerate(msg_data_list):
|
||||||
|
if msg.id == event.msg_id:
|
||||||
|
assert list(data) == event.data
|
||||||
|
log(f"msg {msg.id}: got correct realtime data {data}")
|
||||||
|
del msg_data_list[i]
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
def test_realtime_sequentially(acf, path_to_webxdc):
|
def test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||||
"""Test two peers trying to establish connection sequentially."""
|
"""Test two peers trying to establish connection sequentially."""
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||||
|
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||||
ac1.create_chat(ac2)
|
ac1.create_chat(ac2)
|
||||||
ac2.create_chat(ac1)
|
ac2.create_chat(ac1)
|
||||||
|
|
||||||
# share a webxdc app between ac1 and ac2
|
# share a webxdc app between ac1 and ac2
|
||||||
ac1_webxdc_msg = acf.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
|
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
|
||||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||||
snapshot = ac2_webxdc_msg.get_snapshot()
|
snapshot = ac2_webxdc_msg.get_snapshot()
|
||||||
assert snapshot.text == "play"
|
assert snapshot.text == "play"
|
||||||
@@ -111,7 +98,7 @@ def test_realtime_sequentially(acf, path_to_webxdc):
|
|||||||
# send iroh announcements sequentially
|
# send iroh announcements sequentially
|
||||||
log("sending ac1 -> ac2 realtime advertisement and additional message")
|
log("sending ac1 -> ac2 realtime advertisement and additional message")
|
||||||
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
|
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||||
acf.send_message(from_account=ac1, to_account=ac2, text="ping1")
|
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
|
||||||
|
|
||||||
log("waiting for incoming message on ac2")
|
log("waiting for incoming message on ac2")
|
||||||
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
|
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
|
||||||
@@ -119,7 +106,7 @@ def test_realtime_sequentially(acf, path_to_webxdc):
|
|||||||
|
|
||||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||||
acf.send_message(from_account=ac2, to_account=ac1, text="ping2")
|
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
|
||||||
|
|
||||||
log("waiting for incoming message on ac1")
|
log("waiting for incoming message on ac1")
|
||||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||||
@@ -130,27 +117,50 @@ def test_realtime_sequentially(acf, path_to_webxdc):
|
|||||||
data = os.urandom(128000)
|
data = os.urandom(128000)
|
||||||
ac1_webxdc_msg.send_webxdc_realtime_data(data)
|
ac1_webxdc_msg.send_webxdc_realtime_data(data)
|
||||||
|
|
||||||
assert ac2.wait_for_realtime_data(ac2_webxdc_msg.id) == data
|
log("ac2: waiting for realtime data")
|
||||||
|
while 1:
|
||||||
|
event = ac2.wait_for_event()
|
||||||
|
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||||
|
assert event.data == list(data)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
def test_realtime_simultaneously(acf, path_to_webxdc):
|
def test_realtime_simultaneously(acfactory, path_to_webxdc):
|
||||||
"""Test two peers trying to establish connection simultaneously."""
|
"""Test two peers trying to establish connection simultaneously."""
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||||
|
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||||
|
|
||||||
|
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||||
|
|
||||||
|
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
|
||||||
|
wait_receive_realtime_data([(ac2_webxdc_msg, [10])])
|
||||||
|
|
||||||
|
|
||||||
def test_two_parallel_realtime_simultaneously(acf, path_to_webxdc):
|
def test_two_parallel_realtime_simultaneously(acfactory, path_to_webxdc):
|
||||||
"""Test two peers trying to establish connection simultaneously."""
|
"""Test two peers trying to establish connection simultaneously."""
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=False)
|
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||||
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=False)
|
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||||
wait_realtime_connected([(ac1_webxdc_msg, ac2_webxdc_msg), (ac2_webxdc_msg, ac1_webxdc_msg)])
|
|
||||||
wait_realtime_connected([(ac1_webxdc_msg2, ac2_webxdc_msg2), (ac2_webxdc_msg2, ac1_webxdc_msg2)])
|
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||||
|
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||||
|
|
||||||
|
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
|
||||||
|
setup_thread_send_realtime_data(ac1_webxdc_msg2, [20])
|
||||||
|
setup_thread_send_realtime_data(ac2_webxdc_msg, [30])
|
||||||
|
setup_thread_send_realtime_data(ac2_webxdc_msg2, [40])
|
||||||
|
|
||||||
|
wait_receive_realtime_data([(ac1_webxdc_msg, [30]), (ac1_webxdc_msg2, [40])])
|
||||||
|
wait_receive_realtime_data([(ac2_webxdc_msg, [10]), (ac2_webxdc_msg2, [20])])
|
||||||
|
|
||||||
|
|
||||||
def test_no_duplicate_messages(acf, path_to_webxdc):
|
def test_no_duplicate_messages(acfactory, path_to_webxdc):
|
||||||
"""Test that messages are received only once."""
|
"""Test that messages are received only once."""
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||||
|
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||||
|
|
||||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||||
|
|
||||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="webxdc", file=path_to_webxdc)
|
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="webxdc", file=path_to_webxdc)
|
||||||
@@ -164,29 +174,50 @@ def test_no_duplicate_messages(acf, path_to_webxdc):
|
|||||||
ac2_webxdc_msg.send_webxdc_realtime_data.future(b"foobar")
|
ac2_webxdc_msg.send_webxdc_realtime_data.future(b"foobar")
|
||||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||||
|
|
||||||
with send_realtime_data_forever([ac1_webxdc_msg], data=[lambda i: str(i).encode()]):
|
def thread_run():
|
||||||
n = int(ac2.wait_for_realtime_data(ac2_webxdc_msg.id).decode())
|
for i in range(10):
|
||||||
assert int(ac2.wait_for_realtime_data(ac2_webxdc_msg.id).decode()) > n
|
data = str(i).encode()
|
||||||
|
ac1_webxdc_msg.send_webxdc_realtime_data(data)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
threading.Thread(target=thread_run, daemon=True).start()
|
||||||
|
|
||||||
|
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
|
||||||
|
n = int(bytes(event.data).decode())
|
||||||
|
|
||||||
|
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
|
||||||
|
assert int(bytes(event.data).decode()) > n
|
||||||
|
|
||||||
|
|
||||||
def test_no_reordering(acf, path_to_webxdc):
|
def test_no_reordering(acfactory, path_to_webxdc):
|
||||||
"""Test that sending a lot of realtime messages does not result in reordering."""
|
"""Test that sending a lot of realtime messages does not result in reordering."""
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True)
|
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||||
|
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||||
|
|
||||||
|
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||||
|
|
||||||
|
setup_thread_send_realtime_data(ac1_webxdc_msg, b"hello")
|
||||||
|
wait_receive_realtime_data([(ac2_webxdc_msg, b"hello")])
|
||||||
|
|
||||||
for i in range(200):
|
for i in range(200):
|
||||||
ac1_webxdc_msg.send_webxdc_realtime_data([i])
|
ac1_webxdc_msg.send_webxdc_realtime_data([i])
|
||||||
|
|
||||||
for i in range(200):
|
for i in range(200):
|
||||||
# lingering SETUP_DATA payloads from the wait_realtime_connected() barrier may still arrive
|
while 1:
|
||||||
while (data := ac2.wait_for_realtime_data(ac2_webxdc_msg.id)) == SETUP_DATA:
|
event = ac2.wait_for_event()
|
||||||
pass
|
if event.kind == EventType.WEBXDC_REALTIME_DATA and bytes(event.data) != b"hello":
|
||||||
assert data == bytes([i]), "Reordering detected"
|
if event.data[0] == i:
|
||||||
|
break
|
||||||
|
pytest.fail("Reordering detected")
|
||||||
|
|
||||||
|
|
||||||
def test_advertisement_after_chatting(acf, path_to_webxdc):
|
def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
||||||
"""Test that realtime advertisement is assigned to the correct message after chatting."""
|
"""Test that realtime advertisement is assigned to the correct message after chatting."""
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||||
|
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||||
|
|
||||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
|
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
|
||||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||||
@@ -205,14 +236,17 @@ def test_advertisement_after_chatting(acf, path_to_webxdc):
|
|||||||
assert event.msg_id == ac1_webxdc_msg.id
|
assert event.msg_id == ac1_webxdc_msg.id
|
||||||
|
|
||||||
|
|
||||||
def test_realtime_large_webxdc(acf, path_to_large_webxdc):
|
def test_realtime_large_webxdc(acfactory, path_to_large_webxdc):
|
||||||
"""Tests initializing realtime channel on a large webxdc.
|
"""Tests initializing realtime channel on a large webxdc.
|
||||||
|
|
||||||
This is a regression test for a bug that existed in version 2.42.0.
|
This is a regression test for a bug that existed in version 2.42.0.
|
||||||
Large webxdc is split into pre- and post- message,
|
Large webxdc is split into pre- and post- message,
|
||||||
and this previously resulted in failure to initialize realtime.
|
and this previously resulted in failure to initialize realtime.
|
||||||
"""
|
"""
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||||
|
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||||
|
|
||||||
ac2.create_chat(ac1)
|
ac2.create_chat(ac1)
|
||||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="realtime check", file=path_to_large_webxdc)
|
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="realtime check", file=path_to_large_webxdc)
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
def test_set_location(dc, acf) -> None:
|
def test_set_location(dc, acfactory) -> None:
|
||||||
# Try setting location without any accounts.
|
# Try setting location without any accounts.
|
||||||
assert not dc.set_location(1.0, 2.0, 0.1)
|
assert not dc.set_location(1.0, 2.0, 0.1)
|
||||||
|
|
||||||
# Create one account that does not stream,
|
# Create one account that does not stream,
|
||||||
# set location.
|
# set location.
|
||||||
acf.new_configured_account()
|
acfactory.new_configured_account()
|
||||||
assert not dc.set_location(3.0, 4.0, 0.1)
|
assert not dc.set_location(3.0, 4.0, 0.1)
|
||||||
|
|
||||||
|
|
||||||
def test_send_locations_to_chat(dc, acf):
|
def test_send_locations_to_chat(dc, acfactory):
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
assert not alice.is_sending_locations()
|
assert not alice.is_sending_locations()
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
def anonymous_mib(pid):
|
|
||||||
with open(f"/proc/{pid}/smaps_rollup") as f:
|
|
||||||
for line in f:
|
|
||||||
if line.startswith("Anonymous:"):
|
|
||||||
return int(line.split()[1]) // 1024
|
|
||||||
raise LookupError("Anonymous")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.platform != "linux", reason="reads /proc")
|
|
||||||
def test_attachment_memory_is_returned(acf, rpc, tmp_path):
|
|
||||||
# See also comments for `tune_malloc` in `deltachat-rpc-server/src/main.rs`
|
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
|
||||||
chat1 = acf.get_accepted_chat(ac1, ac2)
|
|
||||||
chat2 = ac2.create_chat(ac1)
|
|
||||||
blob = tmp_path / "blob.bin"
|
|
||||||
blob.write_bytes(os.urandom(20 << 20))
|
|
||||||
before = anonymous_mib(rpc.process.pid)
|
|
||||||
|
|
||||||
for sender_chat, receiver in ((chat1, ac2), (chat2, ac1)):
|
|
||||||
sender_chat.send_file(str(blob))
|
|
||||||
event = receiver.wait_for_incoming_msg_event()
|
|
||||||
assert receiver.get_message_by_id(event.msg_id).get_snapshot().file_bytes == 20 << 20
|
|
||||||
for ac in (ac1, ac2):
|
|
||||||
rpc.wait_for_all_work_done(ac.id)
|
|
||||||
|
|
||||||
grown = anonymous_mib(rpc.process.pid) - before
|
|
||||||
assert grown < 64, f"the server kept {grown} MiB after two 20 MiB attachments"
|
|
||||||
@@ -4,8 +4,8 @@ from deltachat_rpc_client import EventType
|
|||||||
from deltachat_rpc_client.const import MessageState
|
from deltachat_rpc_client.const import MessageState
|
||||||
|
|
||||||
|
|
||||||
def test_bcc_self_is_enabled_when_setting_up_second_device(acf):
|
def test_bcc_self_is_enabled_when_setting_up_second_device(acfactory):
|
||||||
ac = acf.get_online_account()
|
ac = acfactory.get_online_account()
|
||||||
|
|
||||||
# Initially after getting online
|
# Initially after getting online
|
||||||
# the setting bcc_self is set to 0 because there is only one device
|
# the setting bcc_self is set to 0 because there is only one device
|
||||||
@@ -29,22 +29,27 @@ def test_bcc_self_is_enabled_when_setting_up_second_device(acf):
|
|||||||
assert ac.get_config("bcc_self") == "1"
|
assert ac.get_config("bcc_self") == "1"
|
||||||
|
|
||||||
|
|
||||||
def test_one_account_send_bcc_setting(acf, log, direct_imap):
|
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
ac1_clone = ac1.clone()
|
ac1_clone = ac1.clone()
|
||||||
ac1_clone.bring_online()
|
ac1_clone.bring_online()
|
||||||
|
|
||||||
log.section("send out message without bcc to ourselves")
|
log.section("send out message without bcc to ourselves")
|
||||||
ac1.set_config("bcc_self", "0")
|
ac1.set_config("bcc_self", "0")
|
||||||
chat = ac1.create_chat(ac2)
|
chat = ac1.create_chat(ac2)
|
||||||
|
self_addr = ac1.get_config("addr")
|
||||||
|
other_addr = ac2.get_config("addr")
|
||||||
|
|
||||||
msg_out = chat.send_text("message1")
|
msg_out = chat.send_text("message1")
|
||||||
assert not msg_out.get_snapshot().is_forwarded
|
assert not msg_out.get_snapshot().is_forwarded
|
||||||
|
|
||||||
# wait for send out (no BCC)
|
# wait for send out (no BCC)
|
||||||
ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||||
assert ac1.get_config("bcc_self") == "0"
|
assert ac1.get_config("bcc_self") == "0"
|
||||||
|
|
||||||
|
assert self_addr not in ev.msg
|
||||||
|
assert other_addr in ev.msg
|
||||||
|
|
||||||
log.section("ac1: setting bcc_self=1")
|
log.section("ac1: setting bcc_self=1")
|
||||||
ac1.set_config("bcc_self", "1")
|
ac1.set_config("bcc_self", "1")
|
||||||
|
|
||||||
@@ -52,16 +57,20 @@ def test_one_account_send_bcc_setting(acf, log, direct_imap):
|
|||||||
msg_out = chat.send_text("message2")
|
msg_out = chat.send_text("message2")
|
||||||
|
|
||||||
# wait for send out (BCC)
|
# wait for send out (BCC)
|
||||||
ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||||
assert ac1.get_config("bcc_self") == "1"
|
assert ac1.get_config("bcc_self") == "1"
|
||||||
|
|
||||||
# Second client receives only the second message, but not the first.
|
# Second client receives only second message, but not the first.
|
||||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted."
|
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted."
|
||||||
|
|
||||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
|
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
|
||||||
|
|
||||||
|
# now make sure we are sending message to ourselves too
|
||||||
|
assert self_addr in ev.msg
|
||||||
|
assert self_addr in ev.msg
|
||||||
|
|
||||||
# BCC-self messages are marked as seen by the sender device.
|
# BCC-self messages are marked as seen by the sender device.
|
||||||
while True:
|
while True:
|
||||||
event = ac1.wait_for_event()
|
event = ac1.wait_for_event()
|
||||||
@@ -75,9 +84,9 @@ def test_one_account_send_bcc_setting(acf, log, direct_imap):
|
|||||||
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True)))) == 1
|
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_multidevice_sync_seen(acf, log):
|
def test_multidevice_sync_seen(acfactory, log):
|
||||||
"""Test that message marked as seen on one device is marked as seen on another."""
|
"""Test that message marked as seen on one device is marked as seen on another."""
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
ac1_clone = ac1.clone()
|
ac1_clone = ac1.clone()
|
||||||
ac1_clone.bring_online()
|
ac1_clone.bring_online()
|
||||||
|
|
||||||
@@ -127,34 +136,3 @@ def test_multidevice_sync_seen(acf, log):
|
|||||||
assert ac1_clone_message.get_snapshot().state == MessageState.IN_SEEN
|
assert ac1_clone_message.get_snapshot().state == MessageState.IN_SEEN
|
||||||
# Test that the timer is started on the second device after synchronizing the seen status.
|
# Test that the timer is started on the second device after synchronizing the seen status.
|
||||||
assert "Expires: " in ac1_clone_message.get_info()
|
assert "Expires: " in ac1_clone_message.get_info()
|
||||||
|
|
||||||
|
|
||||||
def test_multidevice_sync_seen_mdns_off(acf, log):
|
|
||||||
"""Test that MDNs to self are sent even if MDNs are disabled."""
|
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
|
||||||
ac1.set_config("mdns_enabled", "0")
|
|
||||||
|
|
||||||
ac1_clone = ac1.clone()
|
|
||||||
ac1_clone.bring_online()
|
|
||||||
|
|
||||||
assert ac1.get_config("bcc_self") == "1"
|
|
||||||
assert ac1.get_config("mdns_enabled") == "0"
|
|
||||||
assert ac1_clone.get_config("bcc_self") == "1"
|
|
||||||
assert ac1_clone.get_config("mdns_enabled") == "0"
|
|
||||||
|
|
||||||
ac1.create_chat(ac2)
|
|
||||||
ac1_clone_chat = ac1_clone.create_chat(ac2)
|
|
||||||
ac2_chat = ac2.create_chat(ac1)
|
|
||||||
|
|
||||||
log.section("Send a message from ac2 to ac1 and check that it's 'fresh'")
|
|
||||||
ac2_chat.send_text("Hi")
|
|
||||||
ac1_message = ac1.wait_for_incoming_msg()
|
|
||||||
ac1_clone_message = ac1_clone.wait_for_incoming_msg()
|
|
||||||
|
|
||||||
ac1_message.mark_seen()
|
|
||||||
assert ac1_message.get_snapshot().state == MessageState.IN_SEEN
|
|
||||||
|
|
||||||
log.section("ac1 clone detects that message is marked as seen")
|
|
||||||
ev = ac1_clone.wait_for_event(EventType.MSGS_NOTICED)
|
|
||||||
assert ev.chat_id == ac1_clone_chat.id
|
|
||||||
assert ac1_clone_message.get_snapshot().state == MessageState.IN_SEEN
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import time
|
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from deltachat_rpc_client import EventType
|
from deltachat_rpc_client import EventType
|
||||||
@@ -8,33 +5,11 @@ from deltachat_rpc_client.const import ChatType, DownloadState
|
|||||||
from deltachat_rpc_client.rpc import JsonRpcError
|
from deltachat_rpc_client.rpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
def alice_with_two_transports_and_bob(acf):
|
def test_add_second_address(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
account = acfactory.new_configured_account()
|
||||||
alice.add_transport_from_qr(acf.get_account_qr())
|
|
||||||
alice.bring_online()
|
|
||||||
return alice, alice.create_chat(bob), bob.create_chat(alice)
|
|
||||||
|
|
||||||
|
|
||||||
def messages_with_text(chat, text):
|
|
||||||
return [msg for msg in chat.get_messages() if msg.get_snapshot().text == text]
|
|
||||||
|
|
||||||
|
|
||||||
def wait_for_imap_message(imap):
|
|
||||||
while not imap.get_all_messages():
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_transports(acf):
|
|
||||||
account = acf.get_unconfigured_account()
|
|
||||||
account.init_transports(acf.get_account_qr())
|
|
||||||
assert len(account.list_transports()) == 1
|
assert len(account.list_transports()) == 1
|
||||||
|
|
||||||
|
qr = acfactory.get_account_qr()
|
||||||
def test_add_second_address(acf) -> None:
|
|
||||||
account = acf.new_configured_account()
|
|
||||||
assert len(account.list_transports()) == 1
|
|
||||||
|
|
||||||
qr = acf.get_account_qr()
|
|
||||||
account.add_transport_from_qr(qr)
|
account.add_transport_from_qr(qr)
|
||||||
assert len(account.list_transports()) == 2
|
assert len(account.list_transports()) == 2
|
||||||
|
|
||||||
@@ -43,24 +18,20 @@ def test_add_second_address(acf) -> None:
|
|||||||
|
|
||||||
first_addr = account.list_transports()[0]["addr"]
|
first_addr = account.list_transports()[0]["addr"]
|
||||||
second_addr = account.list_transports()[1]["addr"]
|
second_addr = account.list_transports()[1]["addr"]
|
||||||
third_addr = account.list_transports()[2]["addr"]
|
|
||||||
|
|
||||||
assert account.get_config("configured_addr") == first_addr
|
# Cannot delete the first address.
|
||||||
account.delete_transport(first_addr)
|
with pytest.raises(JsonRpcError):
|
||||||
assert len(account.list_transports()) == 2
|
account.delete_transport(first_addr)
|
||||||
assert account.get_config("configured_addr") != first_addr
|
|
||||||
|
|
||||||
account.delete_transport(second_addr)
|
account.delete_transport(second_addr)
|
||||||
assert len(account.list_transports()) == 1
|
assert len(account.list_transports()) == 2
|
||||||
|
|
||||||
with pytest.raises(JsonRpcError):
|
|
||||||
account.delete_transport(third_addr)
|
|
||||||
|
|
||||||
|
|
||||||
def test_change_address(acf) -> None:
|
def test_change_address(acfactory) -> None:
|
||||||
"""Test Alice configuring a second transport and removing the first one."""
|
"""Test Alice configuring a second transport and setting it as a primary one."""
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("configured_addr")
|
||||||
bob.create_chat(alice)
|
bob.create_chat(alice)
|
||||||
|
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
@@ -70,18 +41,28 @@ def test_change_address(acf) -> None:
|
|||||||
sender_addr1 = msg1.sender.get_snapshot().address
|
sender_addr1 = msg1.sender.get_snapshot().address
|
||||||
|
|
||||||
alice.stop_io()
|
alice.stop_io()
|
||||||
old_alice_addr = alice.list_transports()[0]["addr"]
|
old_alice_addr = alice.get_config("configured_addr")
|
||||||
alice_vcard = alice.self_contact.make_vcard()
|
alice_vcard = alice.self_contact.make_vcard()
|
||||||
assert old_alice_addr in alice_vcard
|
assert old_alice_addr in alice_vcard
|
||||||
qr = acf.get_account_qr()
|
qr = acfactory.get_account_qr()
|
||||||
alice.add_transport_from_qr(qr)
|
alice.add_transport_from_qr(qr)
|
||||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||||
|
with pytest.raises(JsonRpcError):
|
||||||
|
# Cannot use the address that is not
|
||||||
|
# configured for any transport.
|
||||||
|
alice.set_config("configured_addr", bob_addr)
|
||||||
|
|
||||||
alice.delete_transport(old_alice_addr)
|
# Load old address so it is cached.
|
||||||
|
assert alice.get_config("configured_addr") == old_alice_addr
|
||||||
|
alice.set_config("configured_addr", new_alice_addr)
|
||||||
|
# Make sure that setting `configured_addr` invalidated the cache.
|
||||||
|
assert alice.get_config("configured_addr") == new_alice_addr
|
||||||
|
|
||||||
alice_vcard = alice.self_contact.make_vcard()
|
alice_vcard = alice.self_contact.make_vcard()
|
||||||
assert old_alice_addr not in alice_vcard
|
assert old_alice_addr not in alice_vcard
|
||||||
assert new_alice_addr in alice_vcard
|
assert new_alice_addr in alice_vcard
|
||||||
|
with pytest.raises(JsonRpcError):
|
||||||
|
alice.delete_transport(new_alice_addr)
|
||||||
alice.start_io()
|
alice.start_io()
|
||||||
|
|
||||||
alice_chat_bob.send_text("Hello again!")
|
alice_chat_bob.send_text("Hello again!")
|
||||||
@@ -95,37 +76,18 @@ def test_change_address(acf) -> None:
|
|||||||
assert sender_addr2 == new_alice_addr
|
assert sender_addr2 == new_alice_addr
|
||||||
|
|
||||||
|
|
||||||
def test_remove_transport_keep_messages(acf) -> None:
|
def test_download_on_demand(acfactory) -> None:
|
||||||
"""Test that deleting current sending transport keeps queued messages."""
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
alice, bob = acf.get_online_accounts(2)
|
|
||||||
|
|
||||||
qr = acf.get_account_qr()
|
|
||||||
alice.add_transport_from_qr(qr)
|
|
||||||
|
|
||||||
alice.stop_io()
|
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
|
||||||
alice_chat_bob.send_text("Hello!")
|
|
||||||
|
|
||||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
|
||||||
alice.delete_transport(alice.list_transports()[0]["addr"])
|
|
||||||
alice.start_io()
|
|
||||||
|
|
||||||
bob_msg = bob.wait_for_incoming_msg().get_snapshot()
|
|
||||||
assert bob_msg.sender.get_snapshot().address == new_alice_addr
|
|
||||||
|
|
||||||
|
|
||||||
def test_download_on_demand(acf, rpcdata) -> None:
|
|
||||||
alice, bob = acf.get_online_accounts(2)
|
|
||||||
alice.set_config("download_limit", "1")
|
alice.set_config("download_limit", "1")
|
||||||
|
|
||||||
alice.stop_io()
|
alice.stop_io()
|
||||||
qr = acf.get_account_qr()
|
qr = acfactory.get_account_qr()
|
||||||
alice.add_transport_from_qr(qr)
|
alice.add_transport_from_qr(qr)
|
||||||
alice.start_io()
|
alice.start_io()
|
||||||
|
|
||||||
alice.create_chat(bob)
|
alice.create_chat(bob)
|
||||||
chat_bob_alice = bob.create_chat(alice)
|
chat_bob_alice = bob.create_chat(alice)
|
||||||
chat_bob_alice.send_message(file=rpcdata.get_path("image/screenshot.jpg"))
|
chat_bob_alice.send_message(file="../test-data/image/screenshot.jpg")
|
||||||
msg = alice.wait_for_incoming_msg()
|
msg = alice.wait_for_incoming_msg()
|
||||||
snapshot = msg.get_snapshot()
|
snapshot = msg.get_snapshot()
|
||||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||||
@@ -141,15 +103,15 @@ def test_download_on_demand(acf, rpcdata) -> None:
|
|||||||
assert msg.get_snapshot().download_state == dstate
|
assert msg.get_snapshot().download_state == dstate
|
||||||
|
|
||||||
|
|
||||||
def test_reconfigure_transport(acf) -> None:
|
def test_reconfigure_transport(acfactory) -> None:
|
||||||
"""Test that reconfiguring the transport works."""
|
"""Test that reconfiguring the transport works."""
|
||||||
account = acf.get_online_account()
|
account = acfactory.get_online_account()
|
||||||
|
|
||||||
[transport] = account.list_transports()
|
[transport] = account.list_transports()
|
||||||
account.add_or_update_transport(transport)
|
account.add_or_update_transport(transport)
|
||||||
|
|
||||||
|
|
||||||
def test_transport_synchronization(acf, log) -> None:
|
def test_transport_synchronization(acfactory, log) -> None:
|
||||||
"""Test synchronization of transports between devices."""
|
"""Test synchronization of transports between devices."""
|
||||||
|
|
||||||
def wait_for_io_started(ac):
|
def wait_for_io_started(ac):
|
||||||
@@ -158,24 +120,22 @@ def test_transport_synchronization(acf, log) -> None:
|
|||||||
if "scheduler is running" in ev.msg:
|
if "scheduler is running" in ev.msg:
|
||||||
return
|
return
|
||||||
|
|
||||||
def wait_transports(ac, n):
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
while len(ac.list_transports()) != n:
|
|
||||||
ac.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
|
||||||
|
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
|
||||||
ac1_clone = ac1.clone()
|
ac1_clone = ac1.clone()
|
||||||
ac1_clone.bring_online()
|
ac1_clone.bring_online()
|
||||||
|
|
||||||
qr = acf.get_account_qr()
|
qr = acfactory.get_account_qr()
|
||||||
|
|
||||||
ac1.add_transport_from_qr(qr)
|
ac1.add_transport_from_qr(qr)
|
||||||
wait_transports(ac1_clone, 2)
|
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
wait_for_io_started(ac1_clone)
|
wait_for_io_started(ac1_clone)
|
||||||
assert len(ac1.list_transports()) == 2
|
assert len(ac1.list_transports()) == 2
|
||||||
|
assert len(ac1_clone.list_transports()) == 2
|
||||||
|
|
||||||
ac1_clone.add_transport_from_qr(qr)
|
ac1_clone.add_transport_from_qr(qr)
|
||||||
wait_transports(ac1, 3)
|
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
wait_for_io_started(ac1)
|
wait_for_io_started(ac1)
|
||||||
|
assert len(ac1.list_transports()) == 3
|
||||||
assert len(ac1_clone.list_transports()) == 3
|
assert len(ac1_clone.list_transports()) == 3
|
||||||
|
|
||||||
log.section("ac1 clone removes second transport")
|
log.section("ac1 clone removes second transport")
|
||||||
@@ -183,17 +143,24 @@ def test_transport_synchronization(acf, log) -> None:
|
|||||||
addr3 = transport3["addr"]
|
addr3 = transport3["addr"]
|
||||||
ac1_clone.delete_transport(transport2["addr"])
|
ac1_clone.delete_transport(transport2["addr"])
|
||||||
|
|
||||||
wait_transports(ac1, 2)
|
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
wait_for_io_started(ac1)
|
wait_for_io_started(ac1)
|
||||||
[transport1, transport3] = ac1.list_transports()
|
[transport1, transport3] = ac1.list_transports()
|
||||||
|
|
||||||
log.section("ac1 changes the sending transport")
|
log.section("ac1 changes the primary transport")
|
||||||
ac1.set_config("configured_addr", transport3["addr"])
|
ac1.set_config("configured_addr", transport3["addr"])
|
||||||
|
|
||||||
|
# One event for updated `add_timestamp` of the new primary transport,
|
||||||
|
# one event for the `configured_addr` update.
|
||||||
|
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
|
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
|
[transport1, transport3] = ac1_clone.list_transports()
|
||||||
|
assert ac1_clone.get_config("configured_addr") == addr3
|
||||||
|
|
||||||
log.section("ac1 removes the first transport")
|
log.section("ac1 removes the first transport")
|
||||||
ac1.delete_transport(transport1["addr"])
|
ac1.delete_transport(transport1["addr"])
|
||||||
|
|
||||||
wait_transports(ac1_clone, 1)
|
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
wait_for_io_started(ac1_clone)
|
wait_for_io_started(ac1_clone)
|
||||||
[transport3] = ac1_clone.list_transports()
|
[transport3] = ac1_clone.list_transports()
|
||||||
assert transport3["addr"] == addr3
|
assert transport3["addr"] == addr3
|
||||||
@@ -206,16 +173,15 @@ def test_transport_synchronization(acf, log) -> None:
|
|||||||
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
|
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
|
||||||
|
|
||||||
|
|
||||||
def test_transport_sync_new_as_primary(acf, log) -> None:
|
def test_transport_sync_new_as_primary(acfactory, log) -> None:
|
||||||
"""Test that a transport promoted on one device is usable on other devices."""
|
"""Test synchronization of new transport as primary between devices."""
|
||||||
ac1, bob = acf.get_online_accounts(2)
|
ac1, bob = acfactory.get_online_accounts(2)
|
||||||
ac1_clone = ac1.clone()
|
ac1_clone = ac1.clone()
|
||||||
ac1_clone.bring_online()
|
ac1_clone.bring_online()
|
||||||
|
|
||||||
qr = acf.get_account_qr()
|
qr = acfactory.get_account_qr()
|
||||||
|
|
||||||
ac1.add_transport_from_qr(qr)
|
ac1.add_transport_from_qr(qr)
|
||||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
|
||||||
ac1_transports = ac1.list_transports()
|
ac1_transports = ac1.list_transports()
|
||||||
assert len(ac1_transports) == 2
|
assert len(ac1_transports) == 2
|
||||||
[transport1, transport2] = ac1_transports
|
[transport1, transport2] = ac1_transports
|
||||||
@@ -226,7 +192,10 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
|
|||||||
log.section("ac1 changes the primary transport")
|
log.section("ac1 changes the primary transport")
|
||||||
ac1.set_config("configured_addr", transport2["addr"])
|
ac1.set_config("configured_addr", transport2["addr"])
|
||||||
|
|
||||||
log.section("ac1_clone receives a message via the new transport")
|
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
|
assert ac1_clone.get_config("configured_addr") == transport2["addr"]
|
||||||
|
|
||||||
|
log.section("ac1_clone receives a message via the new primary transport")
|
||||||
ac1_chat = ac1.create_chat(bob)
|
ac1_chat = ac1.create_chat(bob)
|
||||||
ac1_chat.send_text("Hello!")
|
ac1_chat.send_text("Hello!")
|
||||||
bob_chat_id = bob.wait_for_incoming_msg_event().chat_id
|
bob_chat_id = bob.wait_for_incoming_msg_event().chat_id
|
||||||
@@ -236,12 +205,12 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
|
|||||||
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "hello back"
|
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "hello back"
|
||||||
|
|
||||||
|
|
||||||
def test_recognize_self_address(acf) -> None:
|
def test_recognize_self_address(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_chat = bob.create_chat(alice)
|
bob_chat = bob.create_chat(alice)
|
||||||
|
|
||||||
qr = acf.get_account_qr()
|
qr = acfactory.get_account_qr()
|
||||||
alice.add_transport_from_qr(qr)
|
alice.add_transport_from_qr(qr)
|
||||||
|
|
||||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||||
@@ -252,10 +221,10 @@ def test_recognize_self_address(acf) -> None:
|
|||||||
assert msg.chat == alice.create_chat(bob)
|
assert msg.chat == alice.create_chat(bob)
|
||||||
|
|
||||||
|
|
||||||
def test_transport_limit(acf) -> None:
|
def test_transport_limit(acfactory) -> None:
|
||||||
"""Test transports limit."""
|
"""Test transports limit."""
|
||||||
account = acf.get_online_account()
|
account = acfactory.get_online_account()
|
||||||
qr = acf.get_account_qr()
|
qr = acfactory.get_account_qr()
|
||||||
|
|
||||||
limit = 5
|
limit = 5
|
||||||
|
|
||||||
@@ -268,22 +237,22 @@ def test_transport_limit(acf) -> None:
|
|||||||
account.add_transport_from_qr(qr)
|
account.add_transport_from_qr(qr)
|
||||||
|
|
||||||
second_addr = account.list_transports()[1]["addr"]
|
second_addr = account.list_transports()[1]["addr"]
|
||||||
|
|
||||||
account.delete_transport(second_addr)
|
account.delete_transport(second_addr)
|
||||||
|
|
||||||
|
# test that adding a transport after deleting one works again
|
||||||
account.add_transport_from_qr(qr)
|
account.add_transport_from_qr(qr)
|
||||||
with pytest.raises(JsonRpcError):
|
|
||||||
account.add_transport_from_qr(qr)
|
|
||||||
|
|
||||||
|
|
||||||
def test_message_info_imap_urls(acf) -> None:
|
def test_message_info_imap_urls(acfactory) -> None:
|
||||||
"""Test that message info contains IMAP URLs of where the message was received."""
|
"""Test that message info contains IMAP URLs of where the message was received."""
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
qr = acf.get_account_qr()
|
qr = acfactory.get_account_qr()
|
||||||
for _ in range(3):
|
for i in range(3):
|
||||||
alice.add_transport_from_qr(qr)
|
alice.add_transport_from_qr(qr)
|
||||||
# Wait for all transports to go IDLE after adding each one.
|
# Wait for all transports to go IDLE after adding each one.
|
||||||
alice.bring_online()
|
for _ in range(i + 1):
|
||||||
|
alice.bring_online()
|
||||||
|
|
||||||
# Enable multi-device mode so messages are not deleted immediately.
|
# Enable multi-device mode so messages are not deleted immediately.
|
||||||
alice.set_config("bcc_self", "1")
|
alice.set_config("bcc_self", "1")
|
||||||
@@ -313,12 +282,20 @@ def test_message_info_imap_urls(acf) -> None:
|
|||||||
assert f"{new_alice_addr}/INBOX" in msg_info
|
assert f"{new_alice_addr}/INBOX" in msg_info
|
||||||
|
|
||||||
|
|
||||||
def test_remove_primary_transport(acf, log) -> None:
|
def test_remove_primary_transport(acfactory, log) -> None:
|
||||||
"""Test that after removing the primary relay, Alice can still receive messages."""
|
"""Test that after removing the primary relay, Alice can still receive messages."""
|
||||||
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
qr = acfactory.get_account_qr()
|
||||||
|
|
||||||
|
alice.add_transport_from_qr(qr)
|
||||||
|
alice.bring_online()
|
||||||
|
|
||||||
|
bob_chat = bob.create_chat(alice)
|
||||||
|
alice.create_chat(bob)
|
||||||
|
|
||||||
log.section("Alice sets up second transport")
|
log.section("Alice sets up second transport")
|
||||||
[transport1, transport2] = alice.list_transports()
|
[transport1, transport2] = alice.list_transports()
|
||||||
|
alice.set_config("configured_addr", transport2["addr"])
|
||||||
|
|
||||||
bob_chat.send_text("Hello!")
|
bob_chat.send_text("Hello!")
|
||||||
msg1 = alice.wait_for_incoming_msg().get_snapshot()
|
msg1 = alice.wait_for_incoming_msg().get_snapshot()
|
||||||
@@ -326,7 +303,6 @@ def test_remove_primary_transport(acf, log) -> None:
|
|||||||
|
|
||||||
log.section("Alice removes the primary relay")
|
log.section("Alice removes the primary relay")
|
||||||
alice.delete_transport(transport1["addr"])
|
alice.delete_transport(transport1["addr"])
|
||||||
assert alice.get_config("configured_addr") == transport2["addr"]
|
|
||||||
alice.stop_io()
|
alice.stop_io()
|
||||||
alice.start_io()
|
alice.start_io()
|
||||||
|
|
||||||
@@ -334,74 +310,4 @@ def test_remove_primary_transport(acf, log) -> None:
|
|||||||
msg2 = alice.wait_for_incoming_msg().get_snapshot()
|
msg2 = alice.wait_for_incoming_msg().get_snapshot()
|
||||||
assert msg2.text == "Hello again!"
|
assert msg2.text == "Hello again!"
|
||||||
assert msg2.chat.get_basic_snapshot().chat_type == ChatType.SINGLE
|
assert msg2.chat.get_basic_snapshot().chat_type == ChatType.SINGLE
|
||||||
assert msg2.chat == alice_chat
|
assert msg2.chat == alice.create_chat(bob)
|
||||||
|
|
||||||
|
|
||||||
def test_qr_works_after_removing_primary_transport(acf, log) -> None:
|
|
||||||
log.section("Alice setups an account and adds two additional relays")
|
|
||||||
alice = acf.new_configured_account()
|
|
||||||
relay_qr = acf.get_account_qr()
|
|
||||||
alice.add_transport_from_qr(relay_qr)
|
|
||||||
alice.add_transport_from_qr(relay_qr)
|
|
||||||
|
|
||||||
first_addr = alice.list_transports()[0]["addr"]
|
|
||||||
second_addr = alice.list_transports()[1]["addr"]
|
|
||||||
third_addr = alice.list_transports()[2]["addr"]
|
|
||||||
|
|
||||||
log.section("Alice creates a QR code")
|
|
||||||
chat_qr = alice.get_qr_code()
|
|
||||||
chat_qr_unquoted = urllib.parse.unquote(chat_qr)
|
|
||||||
assert f"&a={third_addr}" in chat_qr_unquoted
|
|
||||||
assert f"&r={second_addr},{first_addr}" in chat_qr_unquoted
|
|
||||||
|
|
||||||
log.section("Alice removes the transport named by the a= parameter")
|
|
||||||
alice.delete_transport(third_addr)
|
|
||||||
alice.delete_transport(second_addr)
|
|
||||||
|
|
||||||
log.section("Bob scans the QR code, which still works")
|
|
||||||
alice.bring_online()
|
|
||||||
bob = acf.get_online_account()
|
|
||||||
bob.secure_join(chat_qr)
|
|
||||||
alice.wait_for_securejoin_inviter_success()
|
|
||||||
bob.wait_for_securejoin_joiner_success()
|
|
||||||
|
|
||||||
|
|
||||||
def test_background_fetch_from_second_transport(acf, direct_imap, dc):
|
|
||||||
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
|
|
||||||
[transport1, transport2] = alice.list_transports()
|
|
||||||
assert alice.get_config("configured_addr") == transport1["addr"]
|
|
||||||
|
|
||||||
alice.stop_io()
|
|
||||||
bob_chat.send_text("hello")
|
|
||||||
imap1 = direct_imap(alice, transport1["addr"], transport1["password"])
|
|
||||||
wait_for_imap_message(direct_imap(alice, transport2["addr"], transport2["password"]))
|
|
||||||
wait_for_imap_message(imap1)
|
|
||||||
|
|
||||||
# Leave the message on the second transport only.
|
|
||||||
imap1.delete("1:*")
|
|
||||||
|
|
||||||
dc.background_fetch(300)
|
|
||||||
assert len(messages_with_text(alice_chat, "hello")) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_background_fetch_no_duplicates(acf, direct_imap, dc):
|
|
||||||
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
|
|
||||||
|
|
||||||
alice.stop_io()
|
|
||||||
bob_chat.send_text("hello")
|
|
||||||
for transport in alice.list_transports():
|
|
||||||
wait_for_imap_message(direct_imap(alice, transport["addr"], transport["password"]))
|
|
||||||
|
|
||||||
dc.background_fetch(300)
|
|
||||||
assert len(messages_with_text(alice_chat, "hello")) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_multitransport_mdn(acf):
|
|
||||||
"""Test sending an MDN right after configuring two transports."""
|
|
||||||
alice, bob = acf.get_online_accounts(2)
|
|
||||||
alice.add_transport_from_qr(acf.get_account_qr())
|
|
||||||
alice.bring_online()
|
|
||||||
alice.create_chat(bob)
|
|
||||||
bob_msg = bob.create_chat(alice).send_text("Hello!")
|
|
||||||
alice.wait_for_incoming_msg().mark_seen()
|
|
||||||
assert bob.wait_for_event(EventType.MSG_READ).msg_id == bob_msg.id
|
|
||||||
|
|||||||
@@ -1,34 +1,52 @@
|
|||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||||
from deltachat_rpc_client.const import ChatType
|
from deltachat_rpc_client.const import ChatType
|
||||||
|
from deltachat_rpc_client.rpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
def test_qr_setup_contact(acf) -> None:
|
def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
qr_code = alice.get_qr_code()
|
qr_code = alice.get_qr_code()
|
||||||
bob.secure_join(qr_code)
|
bob.secure_join(qr_code)
|
||||||
|
|
||||||
alice.wait_for_securejoin_inviter_success()
|
alice.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
# Test that Alice verified Bob's profile.
|
||||||
alice_contact_bob = alice.create_contact(bob)
|
alice_contact_bob = alice.create_contact(bob)
|
||||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||||
assert alice_contact_bob_snapshot.e2ee_avail
|
assert alice_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
bob.wait_for_securejoin_joiner_success()
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# Test that Bob verified Alice's profile.
|
||||||
bob_contact_alice = bob.create_contact(alice)
|
bob_contact_alice = bob.create_contact(alice)
|
||||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||||
assert bob_contact_alice_snapshot.e2ee_avail
|
assert bob_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
# Test that if Bob imports a key,
|
||||||
|
# backwards verification is not lost
|
||||||
|
# because default key is not changed.
|
||||||
|
logging.info("Bob 2 is created")
|
||||||
|
bob2 = acfactory.new_configured_account()
|
||||||
|
bob2.export_self_keys(tmp_path)
|
||||||
|
|
||||||
|
logging.info("Bob tries to import a key")
|
||||||
|
# Importing a second key is not allowed.
|
||||||
|
with pytest.raises(JsonRpcError):
|
||||||
|
bob.import_self_keys(tmp_path)
|
||||||
|
|
||||||
|
assert bob.get_config("key_id") == "1"
|
||||||
|
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||||
|
assert bob_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
|
||||||
def test_qr_setup_contact_svg(acf) -> None:
|
def test_qr_setup_contact_svg(acfactory) -> None:
|
||||||
alice = acf.new_configured_account()
|
alice = acfactory.new_configured_account()
|
||||||
_, _, domain = alice.list_transports()[0]["addr"].rpartition("@")
|
_, _, domain = alice.get_config("addr").rpartition("@")
|
||||||
|
|
||||||
_qr_code, svg = alice.get_qr_code_svg()
|
_qr_code, svg = alice.get_qr_code_svg()
|
||||||
|
|
||||||
@@ -41,10 +59,9 @@ def test_qr_setup_contact_svg(acf) -> None:
|
|||||||
assert "Alice" in svg
|
assert "Alice" in svg
|
||||||
|
|
||||||
|
|
||||||
def test_qr_securejoin(acf):
|
def test_qr_securejoin(acfactory):
|
||||||
alice, bob, fiona = acf.get_online_accounts(3)
|
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
alice.set_config("displayname", "Alice")
|
|
||||||
# Setup second device for Alice
|
# Setup second device for Alice
|
||||||
# to test observing securejoin protocol.
|
# to test observing securejoin protocol.
|
||||||
alice2 = alice.clone()
|
alice2 = alice.clone()
|
||||||
@@ -64,24 +81,26 @@ def test_qr_securejoin(acf):
|
|||||||
ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||||
bob.wait_for_securejoin_joiner_success()
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# Test that Alice verified Bob's profile.
|
||||||
alice_contact_bob = alice.create_contact(bob)
|
alice_contact_bob = alice.create_contact(bob)
|
||||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||||
assert alice_contact_bob_snapshot.e2ee_avail
|
assert alice_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "You were added by Alice."
|
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||||
|
|
||||||
|
# Test that Bob verified Alice's profile.
|
||||||
bob_contact_alice = bob.create_contact(alice)
|
bob_contact_alice = bob.create_contact(alice)
|
||||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||||
assert bob_contact_alice_snapshot.e2ee_avail
|
assert bob_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
# Start second Alice device.
|
# Start second Alice device.
|
||||||
# Alice observes the securejoin protocol on the second device.
|
# Alice observes securejoin protocol and verifies Bob on second device.
|
||||||
alice2.start_io()
|
alice2.start_io()
|
||||||
alice2.wait_for_securejoin_inviter_success()
|
alice2.wait_for_securejoin_inviter_success()
|
||||||
alice2_contact_bob = alice2.create_contact(bob)
|
alice2_contact_bob = alice2.create_contact(bob)
|
||||||
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
|
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
|
||||||
assert alice2_contact_bob_snapshot.e2ee_avail
|
assert alice2_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
# The QR code token is synced, so alice2 must be able to handle join requests.
|
# The QR code token is synced, so alice2 must be able to handle join requests.
|
||||||
logging.info("Fiona joins the group via alice2")
|
logging.info("Fiona joins the group via alice2")
|
||||||
@@ -91,33 +110,9 @@ def test_qr_securejoin(acf):
|
|||||||
fiona.wait_for_securejoin_joiner_success()
|
fiona.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
|
||||||
def test_qr_securejoin_request_deleted_on_all_transports(acf):
|
|
||||||
alice, bob = acf.get_online_accounts(2)
|
|
||||||
alice.add_transport_from_qr(acf.get_account_qr())
|
|
||||||
alice.bring_online()
|
|
||||||
alice_chat = alice.create_group("Group")
|
|
||||||
qr_code = alice_chat.get_qr_code()
|
|
||||||
alice2 = alice.clone()
|
|
||||||
|
|
||||||
bob.secure_join(qr_code)
|
|
||||||
alice.wait_for_securejoin_inviter_success()
|
|
||||||
alice_chat.remove_contact(bob)
|
|
||||||
# Stop and start io in order to wait until the remove message is sent out.
|
|
||||||
alice.stop_io()
|
|
||||||
alice.bring_online()
|
|
||||||
|
|
||||||
# Membership timestamps have a resolution of one second.
|
|
||||||
time.sleep(1)
|
|
||||||
# By now, the securejoin message sent by Bob must be deleted on Alice's relays.
|
|
||||||
# Otherwise, alice2 would execute securejoin again and re-add Bob.
|
|
||||||
alice2.bring_online()
|
|
||||||
alice2_chat = alice2.get_chat_by_id(alice_chat.id)
|
|
||||||
assert alice2.create_contact(bob) not in alice2_chat.get_contacts()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("all_devices_online", [True, False])
|
@pytest.mark.parametrize("all_devices_online", [True, False])
|
||||||
def test_qr_securejoin_broadcast(acf, all_devices_online):
|
def test_qr_securejoin_broadcast(acfactory, all_devices_online):
|
||||||
alice, bob, fiona = acf.get_online_accounts(3)
|
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
alice2 = alice.clone()
|
alice2 = alice.clone()
|
||||||
bob2 = bob.clone()
|
bob2 = bob.clone()
|
||||||
@@ -156,9 +151,9 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
|
|||||||
assert snapshot2.chat_id == chat.id
|
assert snapshot2.chat_id == chat.id
|
||||||
|
|
||||||
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
|
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
|
||||||
# Check that the chat partner's key is known.
|
# Check that the chat partner is verified.
|
||||||
contact_snapshot = contact.get_snapshot()
|
contact_snapshot = contact.get_snapshot()
|
||||||
assert contact_snapshot.e2ee_avail
|
assert contact_snapshot.is_verified
|
||||||
|
|
||||||
chat = get_broadcast(ac)
|
chat = get_broadcast(ac)
|
||||||
chat_msgs = chat.get_messages()
|
chat_msgs = chat.get_messages()
|
||||||
@@ -172,13 +167,17 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
|
|||||||
assert "invited you to join this channel" in first_msg.text
|
assert "invited you to join this channel" in first_msg.text
|
||||||
assert first_msg.is_info
|
assert first_msg.is_info
|
||||||
|
|
||||||
if not inviter_side:
|
if inviter_side:
|
||||||
|
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||||
|
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
|
||||||
|
assert member_added_msg.info_contact_id == contact_snapshot.id
|
||||||
|
else:
|
||||||
if chat_msgs[0].get_snapshot().text == "You joined the channel.":
|
if chat_msgs[0].get_snapshot().text == "You joined the channel.":
|
||||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||||
else:
|
else:
|
||||||
member_added_msg = chat_msgs.pop(1).get_snapshot()
|
member_added_msg = chat_msgs.pop(1).get_snapshot()
|
||||||
assert member_added_msg.text == "You joined the channel."
|
assert member_added_msg.text == "You joined the channel."
|
||||||
assert member_added_msg.is_info
|
assert member_added_msg.is_info
|
||||||
|
|
||||||
hello_msg = chat_msgs.pop(0).get_snapshot()
|
hello_msg = chat_msgs.pop(0).get_snapshot()
|
||||||
assert hello_msg.text == "Hello everyone!"
|
assert hello_msg.text == "Hello everyone!"
|
||||||
@@ -243,7 +242,7 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
|
|||||||
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
|
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "You joined the channel."
|
assert snapshot.text == "You joined the channel."
|
||||||
|
|
||||||
get_broadcast(alice2).get_messages()[1].resend()
|
get_broadcast(alice2).get_messages()[2].resend()
|
||||||
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
|
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "Hello everyone!"
|
assert snapshot.text == "Hello everyone!"
|
||||||
|
|
||||||
@@ -253,9 +252,9 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
|
|||||||
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
|
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
|
||||||
|
|
||||||
|
|
||||||
def test_qr_securejoin_contact_request(acf) -> None:
|
def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||||
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
@@ -279,8 +278,8 @@ def test_qr_securejoin_contact_request(acf) -> None:
|
|||||||
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
||||||
|
|
||||||
|
|
||||||
def test_qr_readreceipt(acf) -> None:
|
def test_qr_readreceipt(acfactory) -> None:
|
||||||
alice, bob, charlie = acf.get_online_accounts(3)
|
alice, bob, charlie = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
logging.info("Bob and Charlie setup contact with Alice")
|
logging.info("Bob and Charlie setup contact with Alice")
|
||||||
qr_code = alice.get_qr_code()
|
qr_code = alice.get_qr_code()
|
||||||
@@ -336,24 +335,24 @@ def test_qr_readreceipt(acf) -> None:
|
|||||||
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||||
|
|
||||||
|
|
||||||
def test_setup_contact_resetup(acf) -> None:
|
def test_setup_contact_resetup(acfactory) -> None:
|
||||||
"""Tests that setup contact works after Alice resets the device and changes the key."""
|
"""Tests that setup contact works after Alice resets the device and changes the key."""
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
qr_code = alice.get_qr_code()
|
qr_code = alice.get_qr_code()
|
||||||
bob.secure_join(qr_code)
|
bob.secure_join(qr_code)
|
||||||
bob.wait_for_securejoin_joiner_success()
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
alice = acf.resetup_account(alice)
|
alice = acfactory.resetup_account(alice)
|
||||||
|
|
||||||
qr_code = alice.get_qr_code()
|
qr_code = alice.get_qr_code()
|
||||||
bob.secure_join(qr_code)
|
bob.secure_join(qr_code)
|
||||||
bob.wait_for_securejoin_joiner_success()
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
|
||||||
def test_group_member_added_recovery(acf) -> None:
|
def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||||
"""Tests group recovery after a member resets its key."""
|
"""Tests verified group recovery by reverifying then removing and adding a member back."""
|
||||||
ac1, ac2, ac3 = acf.get_online_accounts(3)
|
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
logging.info("ac1 creates a group")
|
logging.info("ac1 creates a group")
|
||||||
chat = ac1.create_group("Group")
|
chat = ac1.create_group("Group")
|
||||||
@@ -363,7 +362,11 @@ def test_group_member_added_recovery(acf) -> None:
|
|||||||
ac2.secure_join(qr_code)
|
ac2.secure_join(qr_code)
|
||||||
ac2.wait_for_securejoin_joiner_success()
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
logging.info("ac3 joins the group")
|
# ac1 has ac2 directly verified.
|
||||||
|
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||||
|
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||||
|
|
||||||
|
logging.info("ac3 joins verified group")
|
||||||
ac3_chat = ac3.secure_join(qr_code)
|
ac3_chat = ac3.secure_join(qr_code)
|
||||||
ac3.wait_for_securejoin_joiner_success()
|
ac3.wait_for_securejoin_joiner_success()
|
||||||
ac3.wait_for_incoming_msg_event() # Member added
|
ac3.wait_for_incoming_msg_event() # Member added
|
||||||
@@ -371,9 +374,9 @@ def test_group_member_added_recovery(acf) -> None:
|
|||||||
ac3_contact_ac2_old = ac3.create_contact(ac2)
|
ac3_contact_ac2_old = ac3.create_contact(ac2)
|
||||||
|
|
||||||
logging.info("ac2 logs in on a new device")
|
logging.info("ac2 logs in on a new device")
|
||||||
ac2 = acf.resetup_account(ac2)
|
ac2 = acfactory.resetup_account(ac2)
|
||||||
|
|
||||||
logging.info("ac2 scans ac3's QR code again")
|
logging.info("ac2 reverifies with ac3")
|
||||||
qr_code = ac3.get_qr_code()
|
qr_code = ac3.get_qr_code()
|
||||||
ac2.secure_join(qr_code)
|
ac2.secure_join(qr_code)
|
||||||
ac2.wait_for_securejoin_joiner_success()
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
@@ -413,20 +416,28 @@ def test_group_member_added_recovery(acf) -> None:
|
|||||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "Works again!"
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
|
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||||
|
ac1_contact_ac3 = ac1.create_contact(ac3)
|
||||||
|
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
||||||
|
# Until we reset verifications and then send the _verified header,
|
||||||
|
# verification is not gossiped here:
|
||||||
|
assert not ac1_contact_ac2_snapshot.is_verified
|
||||||
|
assert ac1_contact_ac2_snapshot.verifier_id != ac1_contact_ac3.id
|
||||||
|
|
||||||
def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
|
||||||
|
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||||
"""Regression test for
|
"""Regression test for
|
||||||
issue <https://github.com/chatmail/core/issues/4894>.
|
issue <https://github.com/chatmail/core/issues/4894>.
|
||||||
"""
|
"""
|
||||||
ac1, ac2, ac3, ac4 = acf.get_online_accounts(4)
|
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
||||||
|
|
||||||
logging.info("ac3: set up contact with ac2")
|
logging.info("ac3: verify with ac2")
|
||||||
qr_code = ac2.get_qr_code()
|
qr_code = ac2.get_qr_code()
|
||||||
ac3.secure_join(qr_code)
|
ac3.secure_join(qr_code)
|
||||||
ac2.wait_for_securejoin_inviter_success()
|
ac2.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
# in order for ac2 to have pending bobstate with a group
|
# in order for ac2 to have pending bobstate with a verified group
|
||||||
# we first create a fully joined group, and then start
|
# we first create a fully joined verified group, and then start
|
||||||
# joining a second time but interrupt it, to create pending bob state
|
# joining a second time but interrupt it, to create pending bob state
|
||||||
|
|
||||||
logging.info("ac1: create a group that ac2 fully joins")
|
logging.info("ac1: create a group that ac2 fully joins")
|
||||||
@@ -435,7 +446,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
|||||||
ac2.secure_join(qr_code)
|
ac2.secure_join(qr_code)
|
||||||
ac1.wait_for_securejoin_inviter_success()
|
ac1.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
# ensure ac1 can write and ac2 receives messages in the chat
|
# ensure ac1 can write and ac2 receives messages in verified chat
|
||||||
ch1.send_text("ac1 says hello")
|
ch1.send_text("ac1 says hello")
|
||||||
while 1:
|
while 1:
|
||||||
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
|
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
|
||||||
@@ -448,11 +459,11 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
|||||||
ac1.remove()
|
ac1.remove()
|
||||||
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
|
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
|
||||||
|
|
||||||
# we meanwhile expect the ac3/ac2 setup-contact started in the beginning to have completed
|
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
|
||||||
assert ac3.create_contact(ac2).get_snapshot().e2ee_avail
|
assert ac3.create_contact(ac2).get_snapshot().is_verified
|
||||||
assert ac2.create_contact(ac3).get_snapshot().e2ee_avail
|
assert ac2.create_contact(ac3).get_snapshot().is_verified
|
||||||
|
|
||||||
logging.info("ac3: create a group VG with ac2")
|
logging.info("ac3: create a verified group VG with ac2")
|
||||||
vg = ac3.create_group("ac3-created")
|
vg = ac3.create_group("ac3-created")
|
||||||
vg.add_contact(ac3.create_contact(ac2))
|
vg.add_contact(ac3.create_contact(ac2))
|
||||||
|
|
||||||
@@ -473,17 +484,17 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def test_qr_new_group_unblocked(acf):
|
def test_qr_new_group_unblocked(acfactory):
|
||||||
"""Regression test for a bug introduced in core v1.113.0.
|
"""Regression test for a bug introduced in core v1.113.0.
|
||||||
ac2 scans a group QR code created by ac1.
|
ac2 scans a verified group QR code created by ac1.
|
||||||
This results in creation of a blocked single chat with ac1 on ac2,
|
This results in creation of a blocked 1:1 chat with ac1 on ac2,
|
||||||
but ac1 contact is not blocked 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.
|
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.
|
ac2 should receive a message and create a contact request for the group.
|
||||||
Due to a bug previously ac2 created a blocked group.
|
Due to a bug previously ac2 created a blocked group.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
ac1_chat = ac1.create_group("Group for joining")
|
ac1_chat = ac1.create_group("Group for joining")
|
||||||
qr_code = ac1_chat.get_qr_code()
|
qr_code = ac1_chat.get_qr_code()
|
||||||
ac2.secure_join(qr_code)
|
ac2.secure_join(qr_code)
|
||||||
@@ -502,13 +513,13 @@ def test_qr_new_group_unblocked(acf):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="AEAP is disabled for now")
|
@pytest.mark.skip(reason="AEAP is disabled for now")
|
||||||
def test_aeap_flow(acf):
|
def test_aeap_flow_verified(acfactory):
|
||||||
"""Test that a new address is added to a contact when it changes its address."""
|
"""Test that a new address is added to a contact when it changes its address."""
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
addr, password = acf.get_credentials()
|
addr, password = acfactory.get_credentials()
|
||||||
|
|
||||||
logging.info("ac1: create group QR, ac2 scans and joins")
|
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||||
chat = ac1.create_group("hello")
|
chat = ac1.create_group("hello")
|
||||||
qr_code = chat.get_qr_code()
|
qr_code = chat.get_qr_code()
|
||||||
logging.info("ac2: start QR-code based join-group protocol")
|
logging.info("ac2: start QR-code based join-group protocol")
|
||||||
@@ -525,9 +536,11 @@ def test_aeap_flow(acf):
|
|||||||
assert msg_in_1.text == msg_out.text
|
assert msg_in_1.text == msg_out.text
|
||||||
|
|
||||||
logging.info("changing email account")
|
logging.info("changing email account")
|
||||||
old_addr = ac1.list_transports()[0]["addr"]
|
ac1.set_config("addr", addr)
|
||||||
ac1.add_transport_from_qr(acf.get_account_qr())
|
ac1.set_config("mail_pw", password)
|
||||||
ac1.delete_transport(old_addr)
|
ac1.stop_io()
|
||||||
|
ac1.configure()
|
||||||
|
ac1.start_io()
|
||||||
|
|
||||||
logging.info("sending second message")
|
logging.info("sending second message")
|
||||||
msg_out = chat.send_text("changed address").get_snapshot()
|
msg_out = chat.send_text("changed address").get_snapshot()
|
||||||
@@ -542,16 +555,65 @@ def test_aeap_flow(acf):
|
|||||||
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
|
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
|
||||||
|
|
||||||
|
|
||||||
def test_securejoin_after_contact_resetup(acf) -> None:
|
def test_gossip_verification(acfactory) -> None:
|
||||||
"""
|
alice, bob, carol = acfactory.get_online_accounts(3)
|
||||||
Regression test for a bug that prevented joining a group with a QR code
|
|
||||||
if the group already contains a contact with the same address as the inviter,
|
|
||||||
but different key fingerprint while a securejoin with that contact is still pending.
|
|
||||||
"""
|
|
||||||
ac1, ac2, ac3 = acf.get_online_accounts(3)
|
|
||||||
ac3.set_config("displayname", "ac3")
|
|
||||||
|
|
||||||
# ac3 creates a group with ac1.
|
# Bob verifies Alice.
|
||||||
|
qr_code = alice.get_qr_code()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# Bob verifies Carol.
|
||||||
|
qr_code = carol.get_qr_code()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
bob_contact_alice = bob.create_contact(alice, "Alice")
|
||||||
|
bob_contact_carol = bob.create_contact(carol, "Carol")
|
||||||
|
carol_contact_alice = carol.create_contact(alice, "Alice")
|
||||||
|
|
||||||
|
logging.info("Bob creates an Autocrypt group")
|
||||||
|
bob_group_chat = bob.create_group("Autocrypt Group")
|
||||||
|
bob_group_chat.add_contact(bob_contact_alice)
|
||||||
|
bob_group_chat.add_contact(bob_contact_carol)
|
||||||
|
bob_group_chat.send_message(text="Hello Autocrypt group")
|
||||||
|
|
||||||
|
snapshot = carol.wait_for_incoming_msg().get_snapshot()
|
||||||
|
assert snapshot.text == "Hello Autocrypt group"
|
||||||
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
|
# Group propagates verification using Autocrypt-Gossip header.
|
||||||
|
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||||
|
# Until we reset verifications and then send the _verified header,
|
||||||
|
# verification is not gossiped here:
|
||||||
|
assert not carol_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
logging.info("Bob creates a Securejoin group")
|
||||||
|
bob_group_chat = bob.create_group("Securejoin Group")
|
||||||
|
bob_group_chat.add_contact(bob_contact_alice)
|
||||||
|
bob_group_chat.add_contact(bob_contact_carol)
|
||||||
|
bob_group_chat.send_message(text="Hello Securejoin group")
|
||||||
|
|
||||||
|
snapshot = carol.wait_for_incoming_msg().get_snapshot()
|
||||||
|
assert snapshot.text == "Hello Securejoin group"
|
||||||
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
|
# Securejoin propagates verification.
|
||||||
|
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||||
|
# Until we reset verifications and then send the _verified header,
|
||||||
|
# verification is not gossiped here:
|
||||||
|
assert not carol_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
|
||||||
|
def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||||
|
"""
|
||||||
|
Regression test for a bug that prevented joining verified group with a QR code
|
||||||
|
if the group is already created and contains
|
||||||
|
a contact with inconsistent (Autocrypt and verified keys exist but don't match) key state.
|
||||||
|
"""
|
||||||
|
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
|
# ac3 creates protected group with ac1.
|
||||||
ac3_chat = ac3.create_group("Group")
|
ac3_chat = ac3.create_group("Group")
|
||||||
|
|
||||||
# ac1 joins ac3 group.
|
# ac1 joins ac3 group.
|
||||||
@@ -561,27 +623,31 @@ def test_securejoin_after_contact_resetup(acf) -> None:
|
|||||||
|
|
||||||
# ac1 waits for member added message and creates a QR code.
|
# ac1 waits for member added message and creates a QR code.
|
||||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "You were added by ac3."
|
assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
|
||||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||||
|
|
||||||
# ac2 sets up contact with ac1
|
# ac2 verifies ac1
|
||||||
qr_code = ac1.get_qr_code()
|
qr_code = ac1.get_qr_code()
|
||||||
ac2.secure_join(qr_code)
|
ac2.secure_join(qr_code)
|
||||||
ac2.wait_for_securejoin_joiner_success()
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# ac1 is verified for ac2.
|
||||||
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
||||||
assert ac2_contact_ac1.get_snapshot().e2ee_avail
|
assert ac2_contact_ac1.get_snapshot().is_verified
|
||||||
|
|
||||||
# ac1 resetups the account.
|
# ac1 resetups the account.
|
||||||
ac1 = acf.resetup_account(ac1)
|
ac1 = acfactory.resetup_account(ac1)
|
||||||
|
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
||||||
|
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||||
|
|
||||||
# ac1 goes offline.
|
# ac1 goes offline.
|
||||||
ac1.remove()
|
ac1.remove()
|
||||||
|
|
||||||
# Scanning a QR code creates a group with the inviter, here ac1.
|
# Scanning a QR code results in creating an unprotected group with an inviter.
|
||||||
# Normally the securejoin protocol
|
# In this case inviter is ac1 which has an inconsistent key state.
|
||||||
# would complete and "Member added" would arrive,
|
# Normally inviter becomes verified as a result of Securejoin protocol
|
||||||
# but ac1 is offline so it never finishes.
|
# and then the group chat becomes verified when "Member added" is received,
|
||||||
|
# but in this case ac1 is offline and this Securejoin process will never finish.
|
||||||
logging.info("ac2 scans ac1 QR code, this is not expected to finish")
|
logging.info("ac2 scans ac1 QR code, this is not expected to finish")
|
||||||
ac2.secure_join(ac1_qr_code)
|
ac2.secure_join(ac1_qr_code)
|
||||||
|
|
||||||
@@ -598,15 +664,16 @@ def test_securejoin_after_contact_resetup(acf) -> None:
|
|||||||
ac2_chat = snapshot.chat
|
ac2_chat = snapshot.chat
|
||||||
assert len(ac2_chat.get_contacts()) == 3
|
assert len(ac2_chat.get_contacts()) == 3
|
||||||
|
|
||||||
|
# ac1 is still "not verified" for ac2 due to inconsistent state.
|
||||||
|
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||||
|
|
||||||
def test_withdraw_securejoin_qr(acf):
|
|
||||||
alice, bob = acf.get_online_accounts(2)
|
def test_withdraw_securejoin_qr(acfactory):
|
||||||
alice.set_config("displayname", "Alice")
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
bob.set_config("displayname", "Bob")
|
|
||||||
|
|
||||||
logging.info("Alice creates a group")
|
logging.info("Alice creates a group")
|
||||||
alice_chat = alice.create_group("Group")
|
alice_chat = alice.create_group("Group")
|
||||||
logging.info("Bob joins the group")
|
logging.info("Bob joins verified group")
|
||||||
|
|
||||||
qr_code = alice_chat.get_qr_code()
|
qr_code = alice_chat.get_qr_code()
|
||||||
bob_chat = bob.secure_join(qr_code)
|
bob_chat = bob.secure_join(qr_code)
|
||||||
@@ -615,11 +682,11 @@ def test_withdraw_securejoin_qr(acf):
|
|||||||
alice.clear_all_events()
|
alice.clear_all_events()
|
||||||
|
|
||||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "You were added by Alice."
|
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||||
bob_chat.leave()
|
bob_chat.leave()
|
||||||
|
|
||||||
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()
|
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()
|
||||||
assert snapshot.text == "Group left by Bob."
|
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
|
||||||
|
|
||||||
logging.info("Alice withdraws QR code.")
|
logging.info("Alice withdraws QR code.")
|
||||||
qr = alice.check_qr(qr_code)
|
qr = alice.check_qr(qr_code)
|
||||||
@@ -637,25 +704,3 @@ def test_withdraw_securejoin_qr(acf):
|
|||||||
and "Ignoring RequestWithAuth message because of invalid auth code." in event.msg
|
and "Ignoring RequestWithAuth message because of invalid auth code." in event.msg
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def test_qr_scan_updates_new_relay_address(acf):
|
|
||||||
alice, bob = acf.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob_alice_chat = bob.secure_join(alice.get_qr_code())
|
|
||||||
alice.wait_for_securejoin_inviter_success()
|
|
||||||
bob.wait_for_securejoin_joiner_success()
|
|
||||||
|
|
||||||
for ac in [alice, bob]:
|
|
||||||
old_addr = ac.get_config("configured_addr")
|
|
||||||
ac.add_transport_from_qr(acf.get_account_qr())
|
|
||||||
ac.set_config("configured_addr", ac.list_transports()[1]["addr"])
|
|
||||||
ac.delete_transport(old_addr)
|
|
||||||
|
|
||||||
bob.secure_join(alice.get_qr_code())
|
|
||||||
alice.wait_for_securejoin_inviter_success()
|
|
||||||
bob.wait_for_securejoin_joiner_success()
|
|
||||||
|
|
||||||
bob_alice_chat.send_text("hi")
|
|
||||||
snapshot = alice.wait_for_incoming_msg().get_snapshot()
|
|
||||||
assert snapshot.text == "hi"
|
|
||||||
|
|||||||
@@ -35,19 +35,6 @@ def test_sleep(rpc) -> None:
|
|||||||
assert sleep_5_future in pending
|
assert sleep_5_future in pending
|
||||||
|
|
||||||
|
|
||||||
def test_shutdown(rpc) -> None:
|
|
||||||
"""Test RPC client shutdown if RPC server process is terminated.
|
|
||||||
|
|
||||||
This is a regression test, at the time of adding it RPC client
|
|
||||||
did not terminate all threads and python process did not exit.
|
|
||||||
Shutting down all threads is tested implicitly by pytest-timeout.
|
|
||||||
"""
|
|
||||||
rpc.process.kill()
|
|
||||||
rpc.process.wait()
|
|
||||||
with pytest.raises(JsonRpcError):
|
|
||||||
rpc.get_system_info()
|
|
||||||
|
|
||||||
|
|
||||||
def test_email_address_validity(rpc) -> None:
|
def test_email_address_validity(rpc) -> None:
|
||||||
valid_addresses = [
|
valid_addresses = [
|
||||||
"email@example.com",
|
"email@example.com",
|
||||||
@@ -61,8 +48,8 @@ def test_email_address_validity(rpc) -> None:
|
|||||||
assert not rpc.check_email_validity(addr)
|
assert not rpc.check_email_validity(addr)
|
||||||
|
|
||||||
|
|
||||||
def test_acf(acf) -> None:
|
def test_acfactory(acfactory) -> None:
|
||||||
account = acf.new_configured_account()
|
account = acfactory.new_configured_account()
|
||||||
while True:
|
while True:
|
||||||
event = account.wait_for_event()
|
event = account.wait_for_event()
|
||||||
if event.kind == EventType.CONFIGURE_PROGRESS:
|
if event.kind == EventType.CONFIGURE_PROGRESS:
|
||||||
@@ -74,9 +61,9 @@ def test_acf(acf) -> None:
|
|||||||
logging.info("Successful configuration")
|
logging.info("Successful configuration")
|
||||||
|
|
||||||
|
|
||||||
def test_configure_starttls(acf) -> None:
|
def test_configure_starttls(acfactory) -> None:
|
||||||
addr, password = acf.get_credentials()
|
addr, password = acfactory.get_credentials()
|
||||||
account = acf.get_unconfigured_account()
|
account = acfactory.get_unconfigured_account()
|
||||||
account.add_or_update_transport(
|
account.add_or_update_transport(
|
||||||
{
|
{
|
||||||
"addr": addr,
|
"addr": addr,
|
||||||
@@ -88,10 +75,10 @@ def test_configure_starttls(acf) -> None:
|
|||||||
assert account.is_configured()
|
assert account.is_configured()
|
||||||
|
|
||||||
|
|
||||||
def test_lowercase_address(acf) -> None:
|
def test_lowercase_address(acfactory) -> None:
|
||||||
addr, password = acf.get_credentials()
|
addr, password = acfactory.get_credentials()
|
||||||
addr_upper = addr.upper()
|
addr_upper = addr.upper()
|
||||||
account = acf.get_unconfigured_account()
|
account = acfactory.get_unconfigured_account()
|
||||||
account.add_or_update_transport(
|
account.add_or_update_transport(
|
||||||
{
|
{
|
||||||
"addr": addr_upper,
|
"addr": addr_upper,
|
||||||
@@ -116,9 +103,9 @@ def test_lowercase_address(acf) -> None:
|
|||||||
assert addr_upper not in param
|
assert addr_upper not in param
|
||||||
|
|
||||||
|
|
||||||
def test_configure_ip(acf) -> None:
|
def test_configure_ip(acfactory) -> None:
|
||||||
addr, password = acf.get_credentials()
|
addr, password = acfactory.get_credentials()
|
||||||
account = acf.get_unconfigured_account()
|
account = acfactory.get_unconfigured_account()
|
||||||
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
|
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
|
||||||
|
|
||||||
with pytest.raises(JsonRpcError):
|
with pytest.raises(JsonRpcError):
|
||||||
@@ -132,10 +119,10 @@ def test_configure_ip(acf) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_configure_alternative_port(acf) -> None:
|
def test_configure_alternative_port(acfactory) -> None:
|
||||||
"""Test that configuration with alternative port 443 works."""
|
"""Test that configuration with alternative port 443 works."""
|
||||||
addr, password = acf.get_credentials()
|
addr, password = acfactory.get_credentials()
|
||||||
account = acf.get_unconfigured_account()
|
account = acfactory.get_unconfigured_account()
|
||||||
account.add_or_update_transport(
|
account.add_or_update_transport(
|
||||||
{
|
{
|
||||||
"addr": addr,
|
"addr": addr,
|
||||||
@@ -147,9 +134,9 @@ def test_configure_alternative_port(acf) -> None:
|
|||||||
assert account.is_configured()
|
assert account.is_configured()
|
||||||
|
|
||||||
|
|
||||||
def test_list_transports(acf) -> None:
|
def test_list_transports(acfactory) -> None:
|
||||||
addr, password = acf.get_credentials()
|
addr, password = acfactory.get_credentials()
|
||||||
account = acf.get_unconfigured_account()
|
account = acfactory.get_unconfigured_account()
|
||||||
account.add_or_update_transport(
|
account.add_or_update_transport(
|
||||||
{
|
{
|
||||||
"addr": addr,
|
"addr": addr,
|
||||||
@@ -165,10 +152,10 @@ def test_list_transports(acf) -> None:
|
|||||||
assert params["imapUser"] == addr
|
assert params["imapUser"] == addr
|
||||||
|
|
||||||
|
|
||||||
def test_account(acf) -> None:
|
def test_account(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("configured_addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
alice_chat_bob.send_text("Hello!")
|
alice_chat_bob.send_text("Hello!")
|
||||||
@@ -234,32 +221,8 @@ def test_account(acf) -> None:
|
|||||||
alice.stop_io()
|
alice.stop_io()
|
||||||
|
|
||||||
|
|
||||||
def test_mark_fresh_vs_self_mdn(acf) -> None:
|
def test_chat(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
bob.set_config("bcc_self", "1")
|
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob)
|
|
||||||
alice_chat = alice_contact_bob.create_chat()
|
|
||||||
alice_chat.send_text("Hello!")
|
|
||||||
|
|
||||||
event = bob.wait_for_incoming_msg_event()
|
|
||||||
chat_id = event.chat_id
|
|
||||||
msg_id = event.msg_id
|
|
||||||
|
|
||||||
bob_chat = bob.get_chat_by_id(chat_id)
|
|
||||||
message = bob.get_message_by_id(msg_id)
|
|
||||||
bob_chat.accept()
|
|
||||||
bob.mark_seen_messages([message])
|
|
||||||
bob_chat.mark_fresh()
|
|
||||||
assert bob_chat.get_fresh_message_count() == 1
|
|
||||||
alice.wait_for_event(EventType.MSG_READ)
|
|
||||||
alice_chat.send_text("You've read 'Hello!'")
|
|
||||||
bob.wait_for_incoming_msg_event()
|
|
||||||
assert bob_chat.get_fresh_message_count() == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_chat(acf) -> None:
|
|
||||||
alice, bob = acf.get_online_accounts(2)
|
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
@@ -288,7 +251,7 @@ def test_chat(acf) -> None:
|
|||||||
bob_chat_alice.unpin()
|
bob_chat_alice.unpin()
|
||||||
bob_chat_alice.archive()
|
bob_chat_alice.archive()
|
||||||
bob_chat_alice.unarchive()
|
bob_chat_alice.unarchive()
|
||||||
with pytest.raises(JsonRpcError): # can't set name for single chats
|
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
|
||||||
bob_chat_alice.set_name("test")
|
bob_chat_alice.set_name("test")
|
||||||
bob_chat_alice.set_ephemeral_timer(300)
|
bob_chat_alice.set_ephemeral_timer(300)
|
||||||
bob_chat_alice.get_encryption_info()
|
bob_chat_alice.get_encryption_info()
|
||||||
@@ -328,10 +291,10 @@ def test_chat(acf) -> None:
|
|||||||
group.get_locations()
|
group.get_locations()
|
||||||
|
|
||||||
|
|
||||||
def test_contact(acf) -> None:
|
def test_contact(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("configured_addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
|
|
||||||
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
|
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
|
||||||
@@ -345,8 +308,8 @@ def test_contact(acf) -> None:
|
|||||||
alice_contact_bob.create_chat()
|
alice_contact_bob.create_chat()
|
||||||
|
|
||||||
|
|
||||||
def test_message(acf) -> None:
|
def test_message(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
@@ -376,8 +339,8 @@ def test_message(acf) -> None:
|
|||||||
assert reactions == snapshot.reactions
|
assert reactions == snapshot.reactions
|
||||||
|
|
||||||
|
|
||||||
def test_receive_imf_failure(acf) -> None:
|
def test_receive_imf_failure(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
|
||||||
@@ -393,7 +356,7 @@ def test_receive_imf_failure(acf) -> None:
|
|||||||
snapshot.text == "❌ Failed to receive a message:"
|
snapshot.text == "❌ Failed to receive a message:"
|
||||||
" Condition failed: `!context.get_config_bool(Config::SimulateReceiveImfError).await?`."
|
" Condition failed: `!context.get_config_bool(Config::SimulateReceiveImfError).await?`."
|
||||||
f" Core version {version}."
|
f" Core version {version}."
|
||||||
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/"
|
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
|
||||||
)
|
)
|
||||||
|
|
||||||
# The failed message doesn't break the IMAP loop.
|
# The failed message doesn't break the IMAP loop.
|
||||||
@@ -405,8 +368,8 @@ def test_receive_imf_failure(acf) -> None:
|
|||||||
assert snapshot.error is None
|
assert snapshot.error is None
|
||||||
|
|
||||||
|
|
||||||
def test_selfavatar_sync(acf, rpcdata, log) -> None:
|
def test_selfavatar_sync(acfactory, data, log) -> None:
|
||||||
alice = acf.get_online_account()
|
alice = acfactory.get_online_account()
|
||||||
|
|
||||||
log.section("Alice adds a second device")
|
log.section("Alice adds a second device")
|
||||||
alice2 = alice.clone()
|
alice2 = alice.clone()
|
||||||
@@ -415,7 +378,7 @@ def test_selfavatar_sync(acf, rpcdata, log) -> None:
|
|||||||
alice2.start_io()
|
alice2.start_io()
|
||||||
|
|
||||||
log.section("First device changes avatar")
|
log.section("First device changes avatar")
|
||||||
image = rpcdata.get_path("image/avatar1000x1000.jpg")
|
image = data.get_path("image/avatar1000x1000.jpg")
|
||||||
alice.set_config("selfavatar", image)
|
alice.set_config("selfavatar", image)
|
||||||
avatar_config = alice.get_config("selfavatar")
|
avatar_config = alice.get_config("selfavatar")
|
||||||
avatar_hash = os.path.basename(avatar_config)
|
avatar_hash = os.path.basename(avatar_config)
|
||||||
@@ -430,10 +393,11 @@ def test_selfavatar_sync(acf, rpcdata, log) -> None:
|
|||||||
assert avatar_config != avatar_config2
|
assert avatar_config != avatar_config2
|
||||||
|
|
||||||
|
|
||||||
def test_dont_move_sync_msgs(acf, direct_imap):
|
def test_dont_move_sync_msgs(acfactory, direct_imap):
|
||||||
addr, password = acf.get_credentials()
|
addr, password = acfactory.get_credentials()
|
||||||
ac1 = acf.get_unconfigured_account()
|
ac1 = acfactory.get_unconfigured_account()
|
||||||
ac1.set_config("bcc_self", "1")
|
ac1.set_config("bcc_self", "1")
|
||||||
|
ac1.set_config("fix_is_chatmail", "1")
|
||||||
ac1.add_or_update_transport({"addr": addr, "password": password})
|
ac1.add_or_update_transport({"addr": addr, "password": password})
|
||||||
ac1.start_io()
|
ac1.start_io()
|
||||||
ac1_direct_imap = direct_imap(ac1)
|
ac1_direct_imap = direct_imap(ac1)
|
||||||
@@ -460,8 +424,8 @@ def test_dont_move_sync_msgs(acf, direct_imap):
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
def test_reaction_seen_on_another_dev(acf) -> None:
|
def test_reaction_seen_on_another_dev(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
alice2 = alice.clone()
|
alice2 = alice.clone()
|
||||||
alice2.start_io()
|
alice2.start_io()
|
||||||
|
|
||||||
@@ -486,38 +450,9 @@ def test_reaction_seen_on_another_dev(acf) -> None:
|
|||||||
assert chat_id == alice2_chat_bob.id
|
assert chat_id == alice2_chat_bob.id
|
||||||
|
|
||||||
|
|
||||||
def test_2nd_device_events_when_msgs_are_seen(acf) -> None:
|
def test_is_bot(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
|
||||||
alice2 = alice.clone()
|
|
||||||
alice2.start_io()
|
|
||||||
|
|
||||||
# Get an accepted chat, otherwise alice2 won't be notified about the 2nd message.
|
|
||||||
chat_alice2 = alice2.create_chat(bob)
|
|
||||||
chat_id_alice2 = chat_alice2.get_basic_snapshot().id
|
|
||||||
|
|
||||||
chat_bob_alice = bob.create_chat(alice)
|
|
||||||
chat_bob_alice.send_text("Hello!")
|
|
||||||
msg_alice = alice.wait_for_incoming_msg()
|
|
||||||
assert alice2.wait_for_incoming_msg_event().chat_id == chat_id_alice2
|
|
||||||
chat_bob_alice.send_text("What's new?")
|
|
||||||
assert alice2.wait_for_incoming_msg_event().chat_id == chat_id_alice2
|
|
||||||
chat_alice2 = alice2.get_chat_by_id(chat_id_alice2)
|
|
||||||
assert chat_alice2.get_fresh_message_count() == 2
|
|
||||||
|
|
||||||
msg_alice.mark_seen()
|
|
||||||
assert alice2.wait_for_msgs_changed_event().chat_id == chat_id_alice2
|
|
||||||
assert chat_alice2.get_fresh_message_count() == 1
|
|
||||||
|
|
||||||
msg_id = alice.wait_for_msgs_changed_event().msg_id
|
|
||||||
msg = alice.get_message_by_id(msg_id)
|
|
||||||
msg.mark_seen()
|
|
||||||
assert alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id == chat_id_alice2
|
|
||||||
assert chat_alice2.get_fresh_message_count() == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_bot(acf) -> None:
|
|
||||||
"""Test that we can recognize messages submitted by bots."""
|
"""Test that we can recognize messages submitted by bots."""
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
@@ -531,18 +466,18 @@ def test_is_bot(acf) -> None:
|
|||||||
assert snapshot.is_bot
|
assert snapshot.is_bot
|
||||||
|
|
||||||
|
|
||||||
def test_bot(acf) -> None:
|
def test_bot(acfactory) -> None:
|
||||||
mock = MagicMock()
|
mock = MagicMock()
|
||||||
user = (acf.get_online_accounts(1))[0]
|
user = (acfactory.get_online_accounts(1))[0]
|
||||||
bot = acf.new_configured_bot()
|
bot = acfactory.new_configured_bot()
|
||||||
bot2 = acf.new_configured_bot()
|
bot2 = acfactory.new_configured_bot()
|
||||||
|
|
||||||
assert bot.is_configured()
|
assert bot.is_configured()
|
||||||
assert bot.account.get_config("bot") == "1"
|
assert bot.account.get_config("bot") == "1"
|
||||||
|
|
||||||
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
|
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
|
||||||
bot.add_hook(*hook)
|
bot.add_hook(*hook)
|
||||||
event = acf.process_message(from_account=user, to_client=bot, text="Hello!")
|
event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
|
||||||
snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot()
|
snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot()
|
||||||
assert not snapshot.is_bot
|
assert not snapshot.is_bot
|
||||||
mock.hook.assert_called_once_with(event.msg_id)
|
mock.hook.assert_called_once_with(event.msg_id)
|
||||||
@@ -555,28 +490,28 @@ def test_bot(acf) -> None:
|
|||||||
hook = track, events.NewMessage(r"hello")
|
hook = track, events.NewMessage(r"hello")
|
||||||
bot.add_hook(*hook)
|
bot.add_hook(*hook)
|
||||||
bot.add_hook(track, events.NewMessage(command="/help"))
|
bot.add_hook(track, events.NewMessage(command="/help"))
|
||||||
event = acf.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)
|
mock.hook.assert_called_with(event.msg_id)
|
||||||
event = acf.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)
|
mock.hook.assert_called_with(event.msg_id)
|
||||||
acf.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
|
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
|
||||||
acf.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
|
assert len(mock.hook.mock_calls) == 2
|
||||||
bot.remove_hook(*hook)
|
bot.remove_hook(*hook)
|
||||||
|
|
||||||
mock.hook.reset_mock()
|
mock.hook.reset_mock()
|
||||||
acf.process_message(from_account=user, to_client=bot, text="hello")
|
acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||||
event = acf.process_message(from_account=user, to_client=bot, text="/help")
|
event = acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
||||||
mock.hook.assert_called_once_with(event.msg_id)
|
mock.hook.assert_called_once_with(event.msg_id)
|
||||||
|
|
||||||
|
|
||||||
def test_wait_next_messages(acf) -> None:
|
def test_wait_next_messages(acfactory) -> None:
|
||||||
alice = acf.get_online_account()
|
alice = acfactory.get_online_account()
|
||||||
|
|
||||||
# Create a bot account so it does not receive device messages in the beginning.
|
# Create a bot account so it does not receive device messages in the beginning.
|
||||||
addr, password = acf.get_credentials()
|
addr, password = acfactory.get_credentials()
|
||||||
bot = acf.get_unconfigured_account()
|
bot = acfactory.get_unconfigured_account()
|
||||||
bot.set_config("bot", "1")
|
bot.set_config("bot", "1")
|
||||||
bot.add_or_update_transport({"addr": addr, "password": password})
|
bot.add_or_update_transport({"addr": addr, "password": password})
|
||||||
assert bot.is_configured()
|
assert bot.is_configured()
|
||||||
@@ -602,26 +537,27 @@ def test_wait_next_messages(acf) -> None:
|
|||||||
assert snapshot.text == "Hello!"
|
assert snapshot.text == "Hello!"
|
||||||
|
|
||||||
|
|
||||||
def test_import_export_backup(acf, tmp_path) -> None:
|
def test_import_export_backup(acfactory, tmp_path) -> None:
|
||||||
alice = acf.new_configured_account()
|
alice = acfactory.new_configured_account()
|
||||||
alice.export_backup(tmp_path)
|
alice.export_backup(tmp_path)
|
||||||
|
|
||||||
files = list(tmp_path.glob("*.tar"))
|
files = list(tmp_path.glob("*.tar"))
|
||||||
alice2 = acf.get_unconfigured_account()
|
alice2 = acfactory.get_unconfigured_account()
|
||||||
alice2.import_backup(files[0])
|
alice2.import_backup(files[0])
|
||||||
|
|
||||||
assert alice2.manager.get_system_info()
|
assert alice2.manager.get_system_info()
|
||||||
|
|
||||||
|
|
||||||
def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
|
def test_import_export_online_all(acfactory, tmp_path, data, log) -> None:
|
||||||
(ac1, some1) = acf.get_online_accounts(2)
|
(ac1, some1) = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
log.section("create some chat content")
|
log.section("create some chat content")
|
||||||
|
some1_addr = some1.get_config("addr")
|
||||||
chat1 = ac1.create_contact(some1).create_chat()
|
chat1 = ac1.create_contact(some1).create_chat()
|
||||||
chat1.send_text("msg1")
|
chat1.send_text("msg1")
|
||||||
assert len(ac1.get_contacts()) == 1
|
assert len(ac1.get_contacts()) == 1
|
||||||
|
|
||||||
original_image_path = rpcdata.get_path("image/avatar64x64.png")
|
original_image_path = data.get_path("image/avatar64x64.png")
|
||||||
chat1.send_file(str(original_image_path))
|
chat1.send_file(str(original_image_path))
|
||||||
|
|
||||||
# Add another 100KB file that ensures that the progress is smooth enough
|
# Add another 100KB file that ensures that the progress is smooth enough
|
||||||
@@ -634,6 +570,7 @@ def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
|
|||||||
contacts = ac.get_contacts()
|
contacts = ac.get_contacts()
|
||||||
assert len(contacts) == 1
|
assert len(contacts) == 1
|
||||||
contact2 = contacts[0]
|
contact2 = contacts[0]
|
||||||
|
assert contact2.get_snapshot().address == some1_addr
|
||||||
chat2 = contact2.create_chat()
|
chat2 = contact2.create_chat()
|
||||||
messages = chat2.get_messages()
|
messages = chat2.get_messages()
|
||||||
assert len(messages) == 3 + E2EE_INFO_MSGS
|
assert len(messages) == 3 + E2EE_INFO_MSGS
|
||||||
@@ -671,7 +608,7 @@ def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
|
|||||||
ac1.start_io()
|
ac1.start_io()
|
||||||
|
|
||||||
log.section("get fresh empty account")
|
log.section("get fresh empty account")
|
||||||
ac2 = acf.get_unconfigured_account()
|
ac2 = acfactory.get_unconfigured_account()
|
||||||
|
|
||||||
log.section("import backup and check it's proper")
|
log.section("import backup and check it's proper")
|
||||||
ac2.import_backup(files_written[0])
|
ac2.import_backup(files_written[0])
|
||||||
@@ -708,8 +645,8 @@ def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
|
|||||||
assert len(list(backupdir.glob("*.tar"))) == 2
|
assert len(list(backupdir.glob("*.tar"))) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_import_export_keys(acf, tmp_path) -> None:
|
def test_import_export_keys(acfactory, tmp_path) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
alice_chat_bob.send_text("Hello Bob!")
|
alice_chat_bob.send_text("Hello Bob!")
|
||||||
@@ -721,7 +658,7 @@ def test_import_export_keys(acf, tmp_path) -> None:
|
|||||||
alice_keys_path = tmp_path / "alice_keys"
|
alice_keys_path = tmp_path / "alice_keys"
|
||||||
alice_keys_path.mkdir()
|
alice_keys_path.mkdir()
|
||||||
alice.export_self_keys(alice_keys_path)
|
alice.export_self_keys(alice_keys_path)
|
||||||
alice = acf.resetup_account(alice)
|
alice = acfactory.resetup_account(alice)
|
||||||
alice.import_self_keys(alice_keys_path)
|
alice.import_self_keys(alice_keys_path)
|
||||||
|
|
||||||
snapshot.chat.accept()
|
snapshot.chat.accept()
|
||||||
@@ -756,14 +693,31 @@ def test_early_failure(tmp_path) -> None:
|
|||||||
with pytest.raises(JsonRpcError, match="invalid_dir"):
|
with pytest.raises(JsonRpcError, match="invalid_dir"):
|
||||||
rpc.start()
|
rpc.start()
|
||||||
|
|
||||||
# Requests issued after the server exited must fail immediately
|
|
||||||
# instead of waiting forever for the finished reader loop.
|
def test_provider_info(rpc) -> None:
|
||||||
with pytest.raises(JsonRpcError, match="RPC server closed"):
|
account_id = rpc.add_account()
|
||||||
rpc.get_system_info()
|
|
||||||
|
provider_info = rpc.get_provider_info(account_id, "example.org")
|
||||||
|
assert provider_info["id"] == "example.com"
|
||||||
|
|
||||||
|
provider_info = rpc.get_provider_info(account_id, "uep7oiw4ahtaizuloith.org")
|
||||||
|
assert provider_info is None
|
||||||
|
|
||||||
|
# Test MX record resolution.
|
||||||
|
# This previously resulted in Gmail provider
|
||||||
|
# because MX record pointed to google.com domain,
|
||||||
|
# but MX record resolution has been removed.
|
||||||
|
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||||
|
assert provider_info is None
|
||||||
|
|
||||||
|
# Disable MX record resolution.
|
||||||
|
rpc.set_config(account_id, "proxy_enabled", "1")
|
||||||
|
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||||
|
assert provider_info is None
|
||||||
|
|
||||||
|
|
||||||
def test_mdn_doesnt_break_autocrypt(acf) -> None:
|
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
|
|
||||||
@@ -793,10 +747,10 @@ def test_mdn_doesnt_break_autocrypt(acf) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("n_accounts", [3, 2])
|
@pytest.mark.parametrize("n_accounts", [3, 2])
|
||||||
def test_download_limit_chat_assignment(acf, tmp_path, n_accounts):
|
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||||
download_limit = 300000
|
download_limit = 300000
|
||||||
|
|
||||||
alice, *others = acf.get_online_accounts(n_accounts)
|
alice, *others = acfactory.get_online_accounts(n_accounts)
|
||||||
bob = others[0]
|
bob = others[0]
|
||||||
|
|
||||||
alice_group = alice.create_group("test group")
|
alice_group = alice.create_group("test group")
|
||||||
@@ -832,10 +786,10 @@ def test_download_limit_chat_assignment(acf, tmp_path, n_accounts):
|
|||||||
assert snapshot.chat == bob_group
|
assert snapshot.chat == bob_group
|
||||||
|
|
||||||
|
|
||||||
def test_download_small_msg_first(acf, tmp_path):
|
def test_download_small_msg_first(acfactory, tmp_path):
|
||||||
download_limit = 70000
|
download_limit = 70000
|
||||||
|
|
||||||
alice, bob0 = acf.get_online_accounts(2)
|
alice, bob0 = acfactory.get_online_accounts(2)
|
||||||
bob1 = bob0.clone()
|
bob1 = bob0.clone()
|
||||||
bob1.set_config("download_limit", str(download_limit))
|
bob1.set_config("download_limit", str(download_limit))
|
||||||
|
|
||||||
@@ -856,14 +810,14 @@ def test_download_small_msg_first(acf, tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("delete_chat", [False, True])
|
@pytest.mark.parametrize("delete_chat", [False, True])
|
||||||
def test_delete_available_msg(acf, tmp_path, direct_imap, delete_chat):
|
def test_delete_available_msg(acfactory, tmp_path, direct_imap, delete_chat):
|
||||||
"""
|
"""
|
||||||
Tests `DownloadState.AVAILABLE` message deletion on the receiver side.
|
Tests `DownloadState.AVAILABLE` message deletion on the receiver side.
|
||||||
Also tests pre- and post-message deletion on the sender side.
|
Also tests pre- and post-message deletion on the sender side.
|
||||||
"""
|
"""
|
||||||
# Min. UI setting as of v2.35
|
# Min. UI setting as of v2.35
|
||||||
download_limit = 163840
|
download_limit = 163840
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
bob.set_config("download_limit", str(download_limit))
|
bob.set_config("download_limit", str(download_limit))
|
||||||
# Avoid immediate deletion from the server
|
# Avoid immediate deletion from the server
|
||||||
alice.set_config("bcc_self", "1")
|
alice.set_config("bcc_self", "1")
|
||||||
@@ -906,8 +860,8 @@ def test_delete_available_msg(acf, tmp_path, direct_imap, delete_chat):
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def test_delete_fully_downloaded_msg(acf, tmp_path, direct_imap):
|
def test_delete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
# Avoid immediate deletion from the server
|
# Avoid immediate deletion from the server
|
||||||
bob.set_config("bcc_self", "1")
|
bob.set_config("bcc_self", "1")
|
||||||
|
|
||||||
@@ -942,8 +896,8 @@ def test_delete_fully_downloaded_msg(acf, tmp_path, direct_imap):
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def test_imap_autodelete_fully_downloaded_msg(acf, tmp_path, direct_imap):
|
def test_imap_autodelete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
chat_alice = alice.create_chat(bob)
|
chat_alice = alice.create_chat(bob)
|
||||||
path = tmp_path / "large"
|
path = tmp_path / "large"
|
||||||
@@ -971,12 +925,12 @@ def test_imap_autodelete_fully_downloaded_msg(acf, tmp_path, direct_imap):
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def test_markseen_contact_request(acf):
|
def test_markseen_contact_request(acfactory):
|
||||||
"""
|
"""
|
||||||
Test that seen status is synchronized for contact request messages
|
Test that seen status is synchronized for contact request messages
|
||||||
even though read receipt is not sent.
|
even though read receipt is not sent.
|
||||||
"""
|
"""
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
# Bob sets up a second device.
|
# Bob sets up a second device.
|
||||||
bob2 = bob.clone()
|
bob2 = bob.clone()
|
||||||
@@ -995,11 +949,11 @@ def test_markseen_contact_request(acf):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("team_profile", [True, False])
|
@pytest.mark.parametrize("team_profile", [True, False])
|
||||||
def test_no_markseen_in_team_profile(team_profile, acf):
|
def test_no_markseen_in_team_profile(team_profile, acfactory):
|
||||||
"""
|
"""
|
||||||
Test that seen status is synchronized iff `team_profile` isn't set.
|
Test that seen status is synchronized iff `team_profile` isn't set.
|
||||||
"""
|
"""
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
if team_profile:
|
if team_profile:
|
||||||
bob.set_config("team_profile", "1")
|
bob.set_config("team_profile", "1")
|
||||||
|
|
||||||
@@ -1018,11 +972,6 @@ def test_no_markseen_in_team_profile(team_profile, acf):
|
|||||||
|
|
||||||
message.mark_seen()
|
message.mark_seen()
|
||||||
|
|
||||||
# The MDN is queued in `smtp_mdns`, which is drained only after the regular
|
|
||||||
# `smtp` queue, so "Outgoing message" would otherwise overtake it on the wire.
|
|
||||||
# Wait for the read receipt to reach Alice before queueing "Outgoing message".
|
|
||||||
alice.wait_for_event(EventType.MSG_READ)
|
|
||||||
|
|
||||||
# Send a message and wait until it arrives
|
# Send a message and wait until it arrives
|
||||||
# in order to wait until Bob2 gets the markseen message.
|
# in order to wait until Bob2 gets the markseen message.
|
||||||
# This also tests that outgoing messages
|
# This also tests that outgoing messages
|
||||||
@@ -1040,11 +989,11 @@ def test_no_markseen_in_team_profile(team_profile, acf):
|
|||||||
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||||
|
|
||||||
|
|
||||||
def test_read_receipt(acf):
|
def test_read_receipt(acfactory):
|
||||||
"""
|
"""
|
||||||
Test sending a read receipt and ensure it is attributed to the correct contact.
|
Test sending a read receipt and ensure it is attributed to the correct contact.
|
||||||
"""
|
"""
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
alice_contact_bob = alice.create_contact(bob)
|
alice_contact_bob = alice.create_contact(bob)
|
||||||
@@ -1063,15 +1012,15 @@ def test_read_receipt(acf):
|
|||||||
assert read_receipt_cnt == 1
|
assert read_receipt_cnt == 1
|
||||||
|
|
||||||
|
|
||||||
def test_get_http_response(acf):
|
def test_get_http_response(acfactory):
|
||||||
alice = acf.new_configured_account()
|
alice = acfactory.new_configured_account()
|
||||||
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
|
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
|
||||||
assert http_response["mimetype"] == "text/html"
|
assert http_response["mimetype"] == "text/html"
|
||||||
assert b"<title>Example Domain</title>" in base64.b64decode((http_response["blob"] + "==").encode())
|
assert b"<title>Example Domain</title>" in base64.b64decode((http_response["blob"] + "==").encode())
|
||||||
|
|
||||||
|
|
||||||
def test_configured_imap_certificate_checks(acf):
|
def test_configured_imap_certificate_checks(acfactory):
|
||||||
alice = acf.new_configured_account()
|
alice = acfactory.new_configured_account()
|
||||||
|
|
||||||
# Certificate checks should be configured (not None)
|
# Certificate checks should be configured (not None)
|
||||||
assert "cert_strict" in alice.get_info().used_transport_settings
|
assert "cert_strict" in alice.get_info().used_transport_settings
|
||||||
@@ -1090,8 +1039,8 @@ def test_configured_imap_certificate_checks(acf):
|
|||||||
assert "cert_old_automatic" not in alice.get_info().used_transport_settings
|
assert "cert_old_automatic" not in alice.get_info().used_transport_settings
|
||||||
|
|
||||||
|
|
||||||
def test_no_old_msg_is_fresh(acf):
|
def test_no_old_msg_is_fresh(acfactory):
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
ac1_clone = ac1.clone()
|
ac1_clone = ac1.clone()
|
||||||
ac1_clone.start_io()
|
ac1_clone.start_io()
|
||||||
|
|
||||||
@@ -1118,9 +1067,9 @@ def test_no_old_msg_is_fresh(acf):
|
|||||||
assert len(list(ac1.get_fresh_messages())) == 0
|
assert len(list(ac1.get_fresh_messages())) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_rename_synchronization(acf):
|
def test_rename_synchronization(acfactory):
|
||||||
"""Test synchronization of contact renaming."""
|
"""Test synchronization of contact renaming."""
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
alice2 = alice.clone()
|
alice2 = alice.clone()
|
||||||
alice2.bring_online()
|
alice2.bring_online()
|
||||||
|
|
||||||
@@ -1135,9 +1084,9 @@ def test_rename_synchronization(acf):
|
|||||||
assert alice2_msg.sender.get_snapshot().display_name == "Bobby"
|
assert alice2_msg.sender.get_snapshot().display_name == "Bobby"
|
||||||
|
|
||||||
|
|
||||||
def test_rename_group(acf):
|
def test_rename_group(acfactory):
|
||||||
"""Test renaming the group."""
|
"""Test renaming the group."""
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
alice_group = alice.create_group("Test group")
|
alice_group = alice.create_group("Test group")
|
||||||
alice_contact_bob = alice.create_contact(bob)
|
alice_contact_bob = alice.create_contact(bob)
|
||||||
@@ -1150,7 +1099,6 @@ def test_rename_group(acf):
|
|||||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||||
|
|
||||||
for name in ["Baz", "Foo bar", "Xyzzy"]:
|
for name in ["Baz", "Foo bar", "Xyzzy"]:
|
||||||
time.sleep(1)
|
|
||||||
alice_group.set_name(name)
|
alice_group.set_name(name)
|
||||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||||
@@ -1166,8 +1114,8 @@ def test_get_all_accounts_deadlock(rpc):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("all_devices_online", [True, False])
|
@pytest.mark.parametrize("all_devices_online", [True, False])
|
||||||
def test_leave_broadcast(acf, all_devices_online):
|
def test_leave_broadcast(acfactory, all_devices_online):
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob2 = bob.clone()
|
bob2 = bob.clone()
|
||||||
|
|
||||||
@@ -1198,6 +1146,7 @@ def test_leave_broadcast(acf, all_devices_online):
|
|||||||
|
|
||||||
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
|
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
|
||||||
chat = get_broadcast(ac)
|
chat = get_broadcast(ac)
|
||||||
|
contact_snapshot = contact.get_snapshot()
|
||||||
chat_msgs = chat.get_messages()
|
chat_msgs = chat.get_messages()
|
||||||
|
|
||||||
encrypted_msg = chat_msgs.pop(0).get_snapshot()
|
encrypted_msg = chat_msgs.pop(0).get_snapshot()
|
||||||
@@ -1209,11 +1158,14 @@ def test_leave_broadcast(acf, all_devices_online):
|
|||||||
assert "invited you to join this channel" in first_msg.text
|
assert "invited you to join this channel" in first_msg.text
|
||||||
assert first_msg.is_info
|
assert first_msg.is_info
|
||||||
|
|
||||||
if not inviter_side:
|
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
if inviter_side:
|
||||||
|
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
|
||||||
|
else:
|
||||||
assert member_added_msg.text == "You joined the channel."
|
assert member_added_msg.text == "You joined the channel."
|
||||||
assert member_added_msg.is_info
|
assert member_added_msg.is_info
|
||||||
|
|
||||||
|
if not inviter_side:
|
||||||
leave_msg = chat_msgs.pop(0).get_snapshot()
|
leave_msg = chat_msgs.pop(0).get_snapshot()
|
||||||
assert leave_msg.text == "You left the channel."
|
assert leave_msg.text == "You left the channel."
|
||||||
|
|
||||||
@@ -1263,8 +1215,8 @@ def test_leave_broadcast(acf, all_devices_online):
|
|||||||
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
|
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
|
||||||
|
|
||||||
|
|
||||||
def test_leave_and_delete_group(acf, log):
|
def test_leave_and_delete_group(acfactory, log):
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
log.section("Alice creates a group")
|
log.section("Alice creates a group")
|
||||||
alice_chat = alice.create_group("Group")
|
alice_chat = alice.create_group("Group")
|
||||||
@@ -1287,12 +1239,12 @@ def test_leave_and_delete_group(acf, log):
|
|||||||
alice.wait_for_event(EventType.CHAT_MODIFIED)
|
alice.wait_for_event(EventType.CHAT_MODIFIED)
|
||||||
|
|
||||||
|
|
||||||
def test_immediate_autodelete(acf, direct_imap, log):
|
def test_immediate_autodelete(acfactory, direct_imap, log):
|
||||||
"""
|
"""
|
||||||
`bcc_self` is off by default,
|
`bcc_self` is off by default,
|
||||||
so that messages are supposed to be immediately autodeleted
|
so that messages are supposed to be immediately autodeleted
|
||||||
"""
|
"""
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
assert ac1.get_config("bcc_self") == "0"
|
assert ac1.get_config("bcc_self") == "0"
|
||||||
|
|
||||||
log.section("ac1: create chat with ac2")
|
log.section("ac1: create chat with ac2")
|
||||||
@@ -1323,8 +1275,8 @@ def test_immediate_autodelete(acf, direct_imap, log):
|
|||||||
assert ev.msg_id == sent_msg.id
|
assert ev.msg_id == sent_msg.id
|
||||||
|
|
||||||
|
|
||||||
def test_background_fetch(acf, dc):
|
def test_background_fetch(acfactory, dc):
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
ac1.stop_io()
|
ac1.stop_io()
|
||||||
|
|
||||||
ac1_chat = ac1.create_chat(ac2)
|
ac1_chat = ac1.create_chat(ac2)
|
||||||
@@ -1360,24 +1312,8 @@ def test_background_fetch(acf, dc):
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def test_background_fetch_does_not_wait_for_sending(dc, acf):
|
def test_message_exists(acfactory):
|
||||||
alice, bob = acf.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
|
||||||
|
|
||||||
alice.stop_io()
|
|
||||||
text = "x" * 200_000
|
|
||||||
for _ in range(50):
|
|
||||||
alice_chat_bob.send_text(text)
|
|
||||||
assert not dc.is_sending_finished()
|
|
||||||
|
|
||||||
alice.start_io()
|
|
||||||
dc.background_fetch(50)
|
|
||||||
dc.wait_for_event(EventType.ACCOUNTS_BACKGROUND_FETCH_DONE)
|
|
||||||
assert not dc.is_sending_finished()
|
|
||||||
|
|
||||||
|
|
||||||
def test_message_exists(acf):
|
|
||||||
ac1, ac2 = acf.get_online_accounts(2)
|
|
||||||
chat = ac1.create_chat(ac2)
|
chat = ac1.create_chat(ac2)
|
||||||
message1 = chat.send_text("Hello!")
|
message1 = chat.send_text("Hello!")
|
||||||
message2 = chat.send_text("Hello again!")
|
message2 = chat.send_text("Hello again!")
|
||||||
@@ -1395,7 +1331,7 @@ def test_message_exists(acf):
|
|||||||
assert not message2.exists()
|
assert not message2.exists()
|
||||||
|
|
||||||
|
|
||||||
def test_synchronize_member_list_on_group_rejoin(acf, log):
|
def test_synchronize_member_list_on_group_rejoin(acfactory, log):
|
||||||
"""
|
"""
|
||||||
Test that user recreates group member list when it joins the group again.
|
Test that user recreates group member list when it joins the group again.
|
||||||
ac1 creates a group with two other accounts: ac2 and ac3
|
ac1 creates a group with two other accounts: ac2 and ac3
|
||||||
@@ -1403,7 +1339,7 @@ def test_synchronize_member_list_on_group_rejoin(acf, log):
|
|||||||
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
|
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
|
||||||
"""
|
"""
|
||||||
log.section("setting up accounts, accepted with each other")
|
log.section("setting up accounts, accepted with each other")
|
||||||
ac1, ac2, ac3 = accounts = acf.get_online_accounts(3)
|
ac1, ac2, ac3 = accounts = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
log.section("ac1: creating group chat with 2 other members")
|
log.section("ac1: creating group chat with 2 other members")
|
||||||
chat = ac1.create_group("title1")
|
chat = ac1.create_group("title1")
|
||||||
@@ -1439,17 +1375,17 @@ def test_synchronize_member_list_on_group_rejoin(acf, log):
|
|||||||
assert msg.get_snapshot().chat.num_contacts() == 2
|
assert msg.get_snapshot().chat.num_contacts() == 2
|
||||||
|
|
||||||
|
|
||||||
def test_large_message(acf, rpcdata) -> None:
|
def test_large_message(acfactory) -> None:
|
||||||
"""
|
"""
|
||||||
Test sending large message without download limit set,
|
Test sending large message without download limit set,
|
||||||
so it is sent with pre-message but downloaded without user interaction.
|
so it is sent with pre-message but downloaded without user interaction.
|
||||||
"""
|
"""
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
alice_chat_bob.send_message(
|
alice_chat_bob.send_message(
|
||||||
"Hello World, this message is bigger than 5 bytes",
|
"Hello World, this message is bigger than 5 bytes",
|
||||||
file=rpcdata.get_path("image/screenshot.jpg"),
|
file="../test-data/image/screenshot.jpg",
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = bob.wait_for_incoming_msg()
|
msg = bob.wait_for_incoming_msg()
|
||||||
@@ -1457,35 +1393,3 @@ def test_large_message(acf, rpcdata) -> None:
|
|||||||
assert msg.id == msgs_changed_event.msg_id
|
assert msg.id == msgs_changed_event.msg_id
|
||||||
snapshot = msg.get_snapshot()
|
snapshot = msg.get_snapshot()
|
||||||
assert snapshot.text == "Hello World, this message is bigger than 5 bytes"
|
assert snapshot.text == "Hello World, this message is bigger than 5 bytes"
|
||||||
|
|
||||||
|
|
||||||
def test_is_sending_finished(dc, acf) -> None:
|
|
||||||
alice, bob = acf.get_online_accounts(2)
|
|
||||||
|
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
|
||||||
bob_chat_alice = bob.create_chat(alice)
|
|
||||||
|
|
||||||
assert dc.is_sending_finished()
|
|
||||||
|
|
||||||
alice_chat_bob.send_text("Hello!")
|
|
||||||
alice.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
|
||||||
|
|
||||||
assert dc.is_sending_finished()
|
|
||||||
|
|
||||||
alice.stop_io()
|
|
||||||
bob.stop_io()
|
|
||||||
|
|
||||||
bob_chat_alice.send_text("Hello back!")
|
|
||||||
alice_chat_bob.send_text("Hello again!")
|
|
||||||
|
|
||||||
assert not dc.is_sending_finished()
|
|
||||||
|
|
||||||
alice.start_io()
|
|
||||||
alice.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
|
||||||
|
|
||||||
assert not dc.is_sending_finished()
|
|
||||||
|
|
||||||
bob.start_io()
|
|
||||||
bob.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
|
||||||
|
|
||||||
assert dc.is_sending_finished()
|
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
"""Test that docs/schema.sql matches the actual database schema."""
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sqlite3
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
DOC_PATH = Path(__file__).resolve().parents[2] / "docs" / "schema.sql"
|
|
||||||
|
|
||||||
|
|
||||||
def strip_comments(sql):
|
|
||||||
return re.sub(r"--[^\n]*", "", sql)
|
|
||||||
|
|
||||||
|
|
||||||
def normalize(stmt):
|
|
||||||
stmt = re.sub(r"\s+", " ", stmt).strip()
|
|
||||||
stmt = stmt.replace("CREATE TABLE IF NOT EXISTS ", "CREATE TABLE ")
|
|
||||||
return re.sub(r'"(\w+)"', r"\1", stmt)
|
|
||||||
|
|
||||||
|
|
||||||
def split_table(body):
|
|
||||||
items = []
|
|
||||||
depth = 0
|
|
||||||
current = ""
|
|
||||||
for char in body:
|
|
||||||
if char == "(":
|
|
||||||
depth += 1
|
|
||||||
elif char == ")":
|
|
||||||
depth -= 1
|
|
||||||
if char == "," and depth == 0:
|
|
||||||
items.append(current.strip())
|
|
||||||
current = ""
|
|
||||||
else:
|
|
||||||
current += char
|
|
||||||
if current.strip():
|
|
||||||
items.append(current.strip())
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
def parse_schema(sql):
|
|
||||||
objects = {}
|
|
||||||
for raw_stmt in strip_comments(sql).split(";"):
|
|
||||||
stmt = normalize(raw_stmt)
|
|
||||||
if not stmt:
|
|
||||||
continue
|
|
||||||
match = re.match(r"CREATE TABLE (\w+) ?\((.*)\)( STRICT)?$", stmt)
|
|
||||||
if match:
|
|
||||||
name, body, strict = match.groups()
|
|
||||||
objects[f"table {name}"] = {
|
|
||||||
"items": sorted(split_table(body)),
|
|
||||||
"strict": bool(strict),
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
match = re.match(r"CREATE (?:UNIQUE )?INDEX (\w+)", stmt)
|
|
||||||
if match:
|
|
||||||
objects[f"index {match.group(1)}"] = {"sql": stmt}
|
|
||||||
continue
|
|
||||||
objects[stmt[:60]] = {"sql": stmt}
|
|
||||||
return objects
|
|
||||||
|
|
||||||
|
|
||||||
def format_diff(documented, real):
|
|
||||||
if "items" in documented and "items" in real:
|
|
||||||
lines = []
|
|
||||||
# disregards order
|
|
||||||
for item in sorted(set(documented["items"]) - set(real["items"])):
|
|
||||||
lines.append(f" documented but not in the database: {item}")
|
|
||||||
for item in sorted(set(real["items"]) - set(documented["items"])):
|
|
||||||
lines.append(f" in the database but not documented: {item}")
|
|
||||||
if documented["strict"] != real["strict"]:
|
|
||||||
lines.append(f" STRICT: documented={documented['strict']} actual={real['strict']}")
|
|
||||||
return "\n".join(lines)
|
|
||||||
return f" documented: {documented}\n actual: {real}"
|
|
||||||
|
|
||||||
|
|
||||||
def read_database_schema(dbfile):
|
|
||||||
with sqlite3.connect(f"file:{dbfile}?mode=ro", uri=True) as conn:
|
|
||||||
rows = conn.execute(
|
|
||||||
"SELECT sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'",
|
|
||||||
).fetchall()
|
|
||||||
return ";\n".join(row[0] for row in rows)
|
|
||||||
|
|
||||||
|
|
||||||
# Tables where every column carries a comment in docs/schema.sql.
|
|
||||||
# Opt-in: document a table's columns, then add it here to lock it in.
|
|
||||||
FULLY_DOCUMENTED_TABLES = {
|
|
||||||
"contacts",
|
|
||||||
"imap_markseen",
|
|
||||||
"multi_device_sync",
|
|
||||||
"transports",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def undocumented_columns(sql, tables):
|
|
||||||
result = []
|
|
||||||
table = None
|
|
||||||
documented = False
|
|
||||||
for line in sql.splitlines():
|
|
||||||
code, _, comment = line.strip().partition("--")
|
|
||||||
code = code.strip()
|
|
||||||
if code.startswith("CREATE TABLE "):
|
|
||||||
name = code.removeprefix("CREATE TABLE ").partition("(")[0].strip()
|
|
||||||
table = name if name in tables else None
|
|
||||||
assert not table or code.endswith("("), f"{code}: want one column per line"
|
|
||||||
elif code.startswith(")"):
|
|
||||||
table = None
|
|
||||||
elif table and code and not re.match(r"(UNIQUE|PRIMARY|FOREIGN|CHECK)\b", code):
|
|
||||||
column = re.match(r"(\w+) (?!INTEGER PRIMARY KEY)", code)
|
|
||||||
if column and not documented and not comment:
|
|
||||||
result.append(f"{table}.{column.group(1)}")
|
|
||||||
documented = bool(comment) and not code
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def test_documented_tables_stay_documented():
|
|
||||||
missing = undocumented_columns(DOC_PATH.read_text(), FULLY_DOCUMENTED_TABLES)
|
|
||||||
assert not missing, "columns without a comment in docs/schema.sql:\n" + "\n".join(missing)
|
|
||||||
|
|
||||||
|
|
||||||
def test_documented_schema_matches_database(acf):
|
|
||||||
account = acf.get_unconfigured_account()
|
|
||||||
real = parse_schema(read_database_schema(account.get_info()["database_dir"]))
|
|
||||||
documented = parse_schema(DOC_PATH.read_text())
|
|
||||||
|
|
||||||
problems = []
|
|
||||||
for name in sorted(real.keys() - documented.keys()):
|
|
||||||
problems.append(f"{name} exists in the database but is not documented")
|
|
||||||
for name in sorted(documented.keys() - real.keys()):
|
|
||||||
problems.append(f"{name} is documented but does not exist in the database")
|
|
||||||
for name in sorted(documented.keys() & real.keys()):
|
|
||||||
if documented[name] != real[name]:
|
|
||||||
problems.append(f"{name} differs:\n{format_diff(documented[name], real[name])}")
|
|
||||||
assert not problems, "documented schema deviates from the database:\n" + "\n".join(problems)
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
def test_vcard(acf) -> None:
|
def test_vcard(acfactory) -> None:
|
||||||
alice, bob, fiona = acf.get_online_accounts(3)
|
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
bob.create_chat(alice)
|
bob.create_chat(alice)
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
def test_webxdc(acf, rpcdata) -> None:
|
def test_webxdc(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
alice_chat_bob.send_message(text="Let's play chess!", file=rpcdata.get_path("webxdc/chess.xdc"))
|
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||||
|
|
||||||
event = bob.wait_for_incoming_msg_event()
|
event = bob.wait_for_incoming_msg_event()
|
||||||
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
||||||
@@ -21,7 +21,7 @@ def test_webxdc(acf, rpcdata) -> None:
|
|||||||
"isAppSender": False,
|
"isAppSender": False,
|
||||||
"isBroadcast": False,
|
"isBroadcast": False,
|
||||||
"sendUpdateInterval": 1000,
|
"sendUpdateInterval": 1000,
|
||||||
"sendUpdateMaxSize": 2**20 * (30 - 1) * 3 // 4,
|
"sendUpdateMaxSize": 18874368,
|
||||||
}
|
}
|
||||||
|
|
||||||
status_updates = message.get_webxdc_status_updates()
|
status_updates = message.get_webxdc_status_updates()
|
||||||
@@ -43,12 +43,12 @@ def test_webxdc(acf, rpcdata) -> None:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_webxdc_insert_lots_of_updates(acf, rpcdata) -> None:
|
def test_webxdc_insert_lots_of_updates(acfactory) -> None:
|
||||||
alice, bob = acf.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
message = alice_chat_bob.send_message(text="Let's play chess!", file=rpcdata.get_path("webxdc/chess.xdc"))
|
message = alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||||
|
|
||||||
for i in range(2000):
|
for i in range(2000):
|
||||||
message.send_webxdc_status_update({"payload": str(i)}, "description")
|
message.send_webxdc_status_update({"payload": str(i)}, "description")
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-rpc-server"
|
name = "deltachat-rpc-server"
|
||||||
version = "2.64.0-dev"
|
version = "2.51.0-dev"
|
||||||
description = "DeltaChat JSON-RPC server"
|
description = "DeltaChat JSON-RPC server"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
|
readme = "README.md"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
|
|
||||||
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
||||||
@@ -14,7 +15,6 @@ deltachat = { workspace = true }
|
|||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
futures-lite = { workspace = true }
|
futures-lite = { workspace = true }
|
||||||
libc = { workspace = true }
|
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Rename the downloaded binary to `deltachat-rpc-server` and add it to your `PATH`
|
|||||||
To install from source run:
|
To install from source run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install --locked --git https://github.com/chatmail/core/ deltachat-rpc-server
|
cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server
|
||||||
```
|
```
|
||||||
|
|
||||||
The `deltachat-rpc-server` executable will be installed into `$HOME/.cargo/bin` that should be available
|
The `deltachat-rpc-server` executable will be installed into `$HOME/.cargo/bin` that should be available
|
||||||
|
|||||||
@@ -15,5 +15,5 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"version": "2.64.0-dev"
|
"version": "2.51.0-dev"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ for (const { folder_name, package_name } of platform_package_names) {
|
|||||||
|
|
||||||
if (is_local) {
|
if (is_local) {
|
||||||
package_json.peerDependencies["@deltachat/jsonrpc-client"] =
|
package_json.peerDependencies["@deltachat/jsonrpc-client"] =
|
||||||
`file:${join(expected_cwd, "/../../deltachat-jsonrpc-bindings/typescript")}`;
|
`file:${join(expected_cwd, "/../../deltachat-jsonrpc/typescript")}`;
|
||||||
} else {
|
} else {
|
||||||
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*";
|
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { ENV_VAR_NAME } from "./const.js";
|
import { ENV_VAR_NAME } from "./const.js";
|
||||||
|
|
||||||
const cargoInstallCommand =
|
const cargoInstallCommand =
|
||||||
"cargo install --locked --git https://github.com/chatmail/core deltachat-rpc-server";
|
"cargo install --git https://github.com/chatmail/core deltachat-rpc-server";
|
||||||
|
|
||||||
export function NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name) {
|
export function NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name) {
|
||||||
return `deltachat-rpc-server not found:
|
return `deltachat-rpc-server not found:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user