mirror of
https://github.com/chatmail/core.git
synced 2026-05-11 19:06:29 +03:00
Compare commits
1 Commits
link2xt/os
...
shadowsock
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a0abee4cb |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -7,8 +7,6 @@ updates:
|
|||||||
commit-message:
|
commit-message:
|
||||||
prefix: "chore(cargo)"
|
prefix: "chore(cargo)"
|
||||||
open-pull-requests-limit: 50
|
open-pull-requests-limit: 50
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|
||||||
# Keep GitHub Actions up to date.
|
# Keep GitHub Actions up to date.
|
||||||
# <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot>
|
# <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot>
|
||||||
@@ -16,5 +14,3 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|||||||
132
.github/workflows/ci.yml
vendored
132
.github/workflows/ci.yml
vendored
@@ -20,29 +20,24 @@ permissions: {}
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: -Dwarnings
|
RUSTFLAGS: -Dwarnings
|
||||||
RUST_VERSION: 1.95.0
|
RUST_VERSION: 1.86.0
|
||||||
|
|
||||||
# Minimum Supported Rust Version
|
# Minimum Supported Rust Version
|
||||||
MSRV: 1.89.0
|
MSRV: 1.82.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint_rust:
|
lint_rust:
|
||||||
name: Lint Rust
|
name: Lint Rust
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install rustfmt and clippy
|
- name: Install rustfmt and clippy
|
||||||
run: rustup toolchain install $RUST_VERSION --profile minimal --component rustfmt --component clippy
|
run: rustup toolchain install $RUST_VERSION --profile minimal --component rustfmt --component clippy
|
||||||
- run: rustup override set $RUST_VERSION
|
|
||||||
shell: bash
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
uses: swatinem/rust-cache@v2
|
||||||
with:
|
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
|
||||||
- name: Run rustfmt
|
- name: Run rustfmt
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
@@ -55,47 +50,40 @@ jobs:
|
|||||||
cargo_deny:
|
cargo_deny:
|
||||||
name: cargo deny
|
name: cargo deny
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb
|
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: --workspace --all-features --locked
|
arguments: --all-features --workspace
|
||||||
command: check
|
command: check
|
||||||
command-arguments: "-Dwarnings"
|
command-arguments: "-Dwarnings"
|
||||||
|
|
||||||
provider_database:
|
provider_database:
|
||||||
name: Check provider database
|
name: Check provider database
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install rustfmt
|
|
||||||
run: rustup component add --toolchain stable-x86_64-unknown-linux-gnu rustfmt
|
|
||||||
- name: Check provider database
|
- name: Check provider database
|
||||||
run: scripts/update-provider-database.sh
|
run: scripts/update-provider-database.sh
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
name: Rust doc comments
|
name: Rust doc comments
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
|
||||||
env:
|
env:
|
||||||
RUSTDOCFLAGS: -Dwarnings
|
RUSTDOCFLAGS: -Dwarnings
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
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@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
uses: swatinem/rust-cache@v2
|
||||||
with:
|
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
|
||||||
- name: Rustdoc
|
- name: Rustdoc
|
||||||
run: cargo doc --document-private-items --no-deps
|
run: cargo doc --document-private-items --no-deps
|
||||||
|
|
||||||
@@ -115,7 +103,6 @@ jobs:
|
|||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
rust: minimum
|
rust: minimum
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- run:
|
- run:
|
||||||
echo "RUSTUP_TOOLCHAIN=$MSRV" >> $GITHUB_ENV
|
echo "RUSTUP_TOOLCHAIN=$MSRV" >> $GITHUB_ENV
|
||||||
@@ -126,7 +113,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
if: matrix.rust == 'latest'
|
if: matrix.rust == 'latest'
|
||||||
|
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -138,24 +125,22 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
uses: swatinem/rust-cache@v2
|
||||||
with:
|
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
|
||||||
|
|
||||||
- name: Install nextest
|
- name: Install nextest
|
||||||
uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458
|
uses: taiki-e/install-action@v2
|
||||||
with:
|
with:
|
||||||
tool: nextest
|
tool: nextest
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
env:
|
env:
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
run: cargo nextest run --workspace --locked
|
run: cargo nextest run --workspace
|
||||||
|
|
||||||
- name: Doc-Tests
|
- name: Doc-Tests
|
||||||
env:
|
env:
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
run: cargo test --workspace --locked --doc
|
run: cargo test --workspace --doc
|
||||||
|
|
||||||
- name: Test cargo vendor
|
- name: Test cargo vendor
|
||||||
run: cargo vendor
|
run: cargo vendor
|
||||||
@@ -166,23 +151,20 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
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@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
uses: swatinem/rust-cache@v2
|
||||||
with:
|
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
|
||||||
|
|
||||||
- name: Build C library
|
- name: Build C library
|
||||||
run: cargo build -p deltachat_ffi
|
run: cargo build -p deltachat_ffi
|
||||||
|
|
||||||
- name: Upload C library
|
- name: Upload C library
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-libdeltachat.a
|
name: ${{ matrix.os }}-libdeltachat.a
|
||||||
path: target/debug/libdeltachat.a
|
path: target/debug/libdeltachat.a
|
||||||
@@ -194,23 +176,20 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
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@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
uses: swatinem/rust-cache@v2
|
||||||
with:
|
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server
|
- name: Build deltachat-rpc-server
|
||||||
run: cargo build -p deltachat-rpc-server
|
run: cargo build -p deltachat-rpc-server
|
||||||
|
|
||||||
- name: Upload deltachat-rpc-server
|
- name: Upload deltachat-rpc-server
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||||
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
||||||
@@ -219,9 +198,8 @@ jobs:
|
|||||||
python_lint:
|
python_lint:
|
||||||
name: Python lint
|
name: Python lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -237,38 +215,6 @@ jobs:
|
|||||||
working-directory: deltachat-rpc-client
|
working-directory: deltachat-rpc-client
|
||||||
run: tox -e lint
|
run: tox -e lint
|
||||||
|
|
||||||
# mypy does not work with PyPy since mypy 1.19
|
|
||||||
# as it introduced native `librt` dependency
|
|
||||||
# that uses CPython internals.
|
|
||||||
# We only run mypy with CPython because of this.
|
|
||||||
cffi_python_mypy:
|
|
||||||
name: CFFI Python mypy
|
|
||||||
needs: ["c_library", "python_lint"]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Download libdeltachat.a
|
|
||||||
uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
name: ubuntu-latest-libdeltachat.a
|
|
||||||
path: target/debug
|
|
||||||
|
|
||||||
- name: Install tox
|
|
||||||
run: pip install tox
|
|
||||||
|
|
||||||
- name: Run mypy
|
|
||||||
env:
|
|
||||||
DCC_RS_TARGET: debug
|
|
||||||
DCC_RS_DEV: ${{ github.workspace }}
|
|
||||||
working-directory: python
|
|
||||||
run: tox -e mypy
|
|
||||||
|
|
||||||
|
|
||||||
cffi_python_tests:
|
cffi_python_tests:
|
||||||
name: CFFI Python tests
|
name: CFFI Python tests
|
||||||
needs: ["c_library", "python_lint"]
|
needs: ["c_library", "python_lint"]
|
||||||
@@ -278,9 +224,9 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
# Currently used Rust version.
|
# Currently used Rust version.
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.14
|
python: 3.13
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: 3.14
|
python: 3.13
|
||||||
|
|
||||||
# PyPy tests
|
# PyPy tests
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
@@ -288,28 +234,27 @@ jobs:
|
|||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: pypy3.10
|
python: pypy3.10
|
||||||
|
|
||||||
# Minimum Supported Python Version = 3.10
|
# Minimum Supported Python Version = 3.8
|
||||||
# This is the minimum version for which manylinux Python wheels are
|
# This is the minimum version for which manylinux Python wheels are
|
||||||
# built. Test it with minimum supported Rust version.
|
# built. Test it with minimum supported Rust version.
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: "3.10"
|
python: 3.8
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download libdeltachat.a
|
- name: Download libdeltachat.a
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-libdeltachat.a
|
name: ${{ matrix.os }}-libdeltachat.a
|
||||||
path: target/debug
|
path: target/debug
|
||||||
|
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
|
||||||
@@ -322,7 +267,7 @@ jobs:
|
|||||||
DCC_RS_TARGET: debug
|
DCC_RS_TARGET: debug
|
||||||
DCC_RS_DEV: ${{ github.workspace }}
|
DCC_RS_DEV: ${{ github.workspace }}
|
||||||
working-directory: python
|
working-directory: python
|
||||||
run: tox -e doc,py
|
run: tox -e mypy,doc,py
|
||||||
|
|
||||||
rpc_python_tests:
|
rpc_python_tests:
|
||||||
name: JSON-RPC Python tests
|
name: JSON-RPC Python tests
|
||||||
@@ -332,11 +277,11 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.14
|
python: 3.13
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: 3.14
|
python: 3.13
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python: 3.14
|
python: 3.13
|
||||||
|
|
||||||
# PyPy tests
|
# PyPy tests
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
@@ -344,20 +289,19 @@ jobs:
|
|||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: pypy3.10
|
python: pypy3.10
|
||||||
|
|
||||||
# Minimum Supported Python Version = 3.10
|
# Minimum Supported Python Version = 3.8
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: "3.10"
|
python: 3.8
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
|
||||||
@@ -365,7 +309,7 @@ jobs:
|
|||||||
run: pip install tox
|
run: pip install tox
|
||||||
|
|
||||||
- name: Download deltachat-rpc-server
|
- name: Download deltachat-rpc-server
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||||
path: target/debug
|
path: target/debug
|
||||||
|
|||||||
257
.github/workflows/deltachat-rpc-server.yml
vendored
257
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -30,46 +30,22 @@ 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@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
- 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
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux
|
name: deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||||
path: result/bin/deltachat-rpc-server
|
path: result/bin/deltachat-rpc-server
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build_linux_wheel:
|
|
||||||
name: Linux wheel
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server wheels
|
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
|
||||||
|
|
||||||
- name: Upload wheel
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
|
||||||
path: result/*.whl
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
build_windows:
|
build_windows:
|
||||||
name: Windows
|
name: Windows
|
||||||
strategy:
|
strategy:
|
||||||
@@ -78,46 +54,22 @@ jobs:
|
|||||||
arch: [win32, win64]
|
arch: [win32, win64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
- 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 }}
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}
|
name: deltachat-rpc-server-${{ matrix.arch }}
|
||||||
path: result/bin/deltachat-rpc-server.exe
|
path: result/bin/deltachat-rpc-server.exe
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build_windows_wheel:
|
|
||||||
name: Windows wheel
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
arch: [win32, win64]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server wheels
|
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
|
||||||
|
|
||||||
- name: Upload wheel
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-wheel
|
|
||||||
path: result/*.whl
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
build_macos:
|
build_macos:
|
||||||
name: macOS
|
name: macOS
|
||||||
strategy:
|
strategy:
|
||||||
@@ -127,7 +79,7 @@ jobs:
|
|||||||
|
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -139,7 +91,7 @@ jobs:
|
|||||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
||||||
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||||
@@ -153,49 +105,25 @@ jobs:
|
|||||||
arch: [arm64-v8a, armeabi-v7a]
|
arch: [arm64-v8a, armeabi-v7a]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
- 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
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
||||||
path: result/bin/deltachat-rpc-server
|
path: result/bin/deltachat-rpc-server
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build_android_wheel:
|
|
||||||
name: Android wheel
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
arch: [arm64-v8a, armeabi-v7a]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server wheels
|
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
|
||||||
|
|
||||||
- name: Upload wheel
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
|
||||||
path: result/*.whl
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Build wheels and upload binaries to the release
|
name: Build wheels and upload binaries to the release
|
||||||
needs: ["build_linux", "build_linux_wheel", "build_windows", "build_windows_wheel", "build_macos", "build_android", "build_android_wheel"]
|
needs: ["build_linux", "build_windows", "build_macos"]
|
||||||
environment:
|
environment:
|
||||||
name: pypi
|
name: pypi
|
||||||
url: https://pypi.org/p/deltachat-rpc-server
|
url: https://pypi.org/p/deltachat-rpc-server
|
||||||
@@ -204,132 +132,78 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
- name: Download Linux aarch64 binary
|
- name: Download Linux aarch64 binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-aarch64-linux
|
name: deltachat-rpc-server-aarch64-linux
|
||||||
path: deltachat-rpc-server-aarch64-linux.d
|
path: deltachat-rpc-server-aarch64-linux.d
|
||||||
|
|
||||||
- name: Download Linux aarch64 wheel
|
|
||||||
uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-aarch64-linux-wheel
|
|
||||||
path: deltachat-rpc-server-aarch64-linux-wheel.d
|
|
||||||
|
|
||||||
- name: Download Linux armv7l binary
|
- name: Download Linux armv7l binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armv7l-linux
|
name: deltachat-rpc-server-armv7l-linux
|
||||||
path: deltachat-rpc-server-armv7l-linux.d
|
path: deltachat-rpc-server-armv7l-linux.d
|
||||||
|
|
||||||
- name: Download Linux armv7l wheel
|
|
||||||
uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-armv7l-linux-wheel
|
|
||||||
path: deltachat-rpc-server-armv7l-linux-wheel.d
|
|
||||||
|
|
||||||
- name: Download Linux armv6l binary
|
- name: Download Linux armv6l binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armv6l-linux
|
name: deltachat-rpc-server-armv6l-linux
|
||||||
path: deltachat-rpc-server-armv6l-linux.d
|
path: deltachat-rpc-server-armv6l-linux.d
|
||||||
|
|
||||||
- name: Download Linux armv6l wheel
|
|
||||||
uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-armv6l-linux-wheel
|
|
||||||
path: deltachat-rpc-server-armv6l-linux-wheel.d
|
|
||||||
|
|
||||||
- name: Download Linux i686 binary
|
- name: Download Linux i686 binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-i686-linux
|
name: deltachat-rpc-server-i686-linux
|
||||||
path: deltachat-rpc-server-i686-linux.d
|
path: deltachat-rpc-server-i686-linux.d
|
||||||
|
|
||||||
- name: Download Linux i686 wheel
|
|
||||||
uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-i686-linux-wheel
|
|
||||||
path: deltachat-rpc-server-i686-linux-wheel.d
|
|
||||||
|
|
||||||
- name: Download Linux x86_64 binary
|
- name: Download Linux x86_64 binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-linux
|
name: deltachat-rpc-server-x86_64-linux
|
||||||
path: deltachat-rpc-server-x86_64-linux.d
|
path: deltachat-rpc-server-x86_64-linux.d
|
||||||
|
|
||||||
- name: Download Linux x86_64 wheel
|
|
||||||
uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-x86_64-linux-wheel
|
|
||||||
path: deltachat-rpc-server-x86_64-linux-wheel.d
|
|
||||||
|
|
||||||
- name: Download Win32 binary
|
- name: Download Win32 binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-win32
|
name: deltachat-rpc-server-win32
|
||||||
path: deltachat-rpc-server-win32.d
|
path: deltachat-rpc-server-win32.d
|
||||||
|
|
||||||
- name: Download Win32 wheel
|
|
||||||
uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-win32-wheel
|
|
||||||
path: deltachat-rpc-server-win32-wheel.d
|
|
||||||
|
|
||||||
- name: Download Win64 binary
|
- name: Download Win64 binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-win64
|
name: deltachat-rpc-server-win64
|
||||||
path: deltachat-rpc-server-win64.d
|
path: deltachat-rpc-server-win64.d
|
||||||
|
|
||||||
- name: Download Win64 wheel
|
|
||||||
uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-win64-wheel
|
|
||||||
path: deltachat-rpc-server-win64-wheel.d
|
|
||||||
|
|
||||||
- name: Download macOS binary for x86_64
|
- name: Download macOS binary for x86_64
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-macos
|
name: deltachat-rpc-server-x86_64-macos
|
||||||
path: deltachat-rpc-server-x86_64-macos.d
|
path: deltachat-rpc-server-x86_64-macos.d
|
||||||
|
|
||||||
- name: Download macOS binary for aarch64
|
- name: Download macOS binary for aarch64
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-aarch64-macos
|
name: deltachat-rpc-server-aarch64-macos
|
||||||
path: deltachat-rpc-server-aarch64-macos.d
|
path: deltachat-rpc-server-aarch64-macos.d
|
||||||
|
|
||||||
- name: Download Android binary for arm64-v8a
|
- name: Download Android binary for arm64-v8a
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-arm64-v8a-android
|
name: deltachat-rpc-server-arm64-v8a-android
|
||||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||||
|
|
||||||
- name: Download Android wheel for arm64-v8a
|
|
||||||
uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-arm64-v8a-android-wheel
|
|
||||||
path: deltachat-rpc-server-arm64-v8a-android-wheel.d
|
|
||||||
|
|
||||||
- name: Download Android binary for armeabi-v7a
|
- name: Download Android binary for armeabi-v7a
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armeabi-v7a-android
|
name: deltachat-rpc-server-armeabi-v7a-android
|
||||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||||
|
|
||||||
- name: Download Android wheel for armeabi-v7a
|
|
||||||
uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-armeabi-v7a-android-wheel
|
|
||||||
path: deltachat-rpc-server-armeabi-v7a-android-wheel.d
|
|
||||||
|
|
||||||
- name: Create bin/ directory
|
- name: Create bin/ directory
|
||||||
run: |
|
run: |
|
||||||
mkdir -p bin
|
mkdir -p bin
|
||||||
@@ -348,21 +222,38 @@ jobs:
|
|||||||
- name: List binaries
|
- name: List binaries
|
||||||
run: ls -l bin/
|
run: ls -l bin/
|
||||||
|
|
||||||
|
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
|
||||||
|
- name: Install python 3.12
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
|
||||||
- name: Install wheel
|
- name: Install wheel
|
||||||
run: pip install wheel
|
run: pip install wheel
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server Python wheels
|
- name: Build deltachat-rpc-server Python wheels and source package
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
mv deltachat-rpc-server-aarch64-linux-wheel.d/*.whl dist/
|
nix build .#deltachat-rpc-server-x86_64-linux-wheel
|
||||||
mv deltachat-rpc-server-armv7l-linux-wheel.d/*.whl dist/
|
cp result/*.whl dist/
|
||||||
mv deltachat-rpc-server-armv6l-linux-wheel.d/*.whl dist/
|
nix build .#deltachat-rpc-server-armv7l-linux-wheel
|
||||||
mv deltachat-rpc-server-i686-linux-wheel.d/*.whl dist/
|
cp result/*.whl dist/
|
||||||
mv deltachat-rpc-server-x86_64-linux-wheel.d/*.whl dist/
|
nix build .#deltachat-rpc-server-armv6l-linux-wheel
|
||||||
mv deltachat-rpc-server-win64-wheel.d/*.whl dist/
|
cp result/*.whl dist/
|
||||||
mv deltachat-rpc-server-win32-wheel.d/*.whl dist/
|
nix build .#deltachat-rpc-server-aarch64-linux-wheel
|
||||||
mv deltachat-rpc-server-arm64-v8a-android-wheel.d/*.whl dist/
|
cp result/*.whl dist/
|
||||||
mv deltachat-rpc-server-armeabi-v7a-android-wheel.d/*.whl dist/
|
nix build .#deltachat-rpc-server-i686-linux-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-win64-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-win32-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-arm64-v8a-android-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-armeabi-v7a-android-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-source
|
||||||
|
cp result/*.tar.gz dist/
|
||||||
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
|
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
|
||||||
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
|
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
|
||||||
mv *.whl dist/
|
mv *.whl dist/
|
||||||
@@ -380,93 +271,90 @@ jobs:
|
|||||||
--repo ${{ github.repository }} \
|
--repo ${{ github.repository }} \
|
||||||
bin/* dist/*
|
bin/* dist/*
|
||||||
|
|
||||||
- name: Publish deltachat-rpc-server to PyPI
|
- name: Publish deltachat-rpc-client to PyPI
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
|
||||||
publish_npm_package:
|
publish_npm_package:
|
||||||
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
||||||
needs: ["build_linux", "build_windows", "build_macos"]
|
needs: ["build_linux", "build_windows", "build_macos"]
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
environment:
|
|
||||||
name: npm-stdio-rpc-server
|
|
||||||
url: https://www.npmjs.com/package/@deltachat/stdio-rpc-server
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
# 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@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-python@v6
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Download Linux aarch64 binary
|
- name: Download Linux aarch64 binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-aarch64-linux
|
name: deltachat-rpc-server-aarch64-linux
|
||||||
path: deltachat-rpc-server-aarch64-linux.d
|
path: deltachat-rpc-server-aarch64-linux.d
|
||||||
|
|
||||||
- name: Download Linux armv7l binary
|
- name: Download Linux armv7l binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armv7l-linux
|
name: deltachat-rpc-server-armv7l-linux
|
||||||
path: deltachat-rpc-server-armv7l-linux.d
|
path: deltachat-rpc-server-armv7l-linux.d
|
||||||
|
|
||||||
- name: Download Linux armv6l binary
|
- name: Download Linux armv6l binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armv6l-linux
|
name: deltachat-rpc-server-armv6l-linux
|
||||||
path: deltachat-rpc-server-armv6l-linux.d
|
path: deltachat-rpc-server-armv6l-linux.d
|
||||||
|
|
||||||
- name: Download Linux i686 binary
|
- name: Download Linux i686 binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-i686-linux
|
name: deltachat-rpc-server-i686-linux
|
||||||
path: deltachat-rpc-server-i686-linux.d
|
path: deltachat-rpc-server-i686-linux.d
|
||||||
|
|
||||||
- name: Download Linux x86_64 binary
|
- name: Download Linux x86_64 binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-linux
|
name: deltachat-rpc-server-x86_64-linux
|
||||||
path: deltachat-rpc-server-x86_64-linux.d
|
path: deltachat-rpc-server-x86_64-linux.d
|
||||||
|
|
||||||
- name: Download Win32 binary
|
- name: Download Win32 binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-win32
|
name: deltachat-rpc-server-win32
|
||||||
path: deltachat-rpc-server-win32.d
|
path: deltachat-rpc-server-win32.d
|
||||||
|
|
||||||
- name: Download Win64 binary
|
- name: Download Win64 binary
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-win64
|
name: deltachat-rpc-server-win64
|
||||||
path: deltachat-rpc-server-win64.d
|
path: deltachat-rpc-server-win64.d
|
||||||
|
|
||||||
- name: Download macOS binary for x86_64
|
- name: Download macOS binary for x86_64
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-macos
|
name: deltachat-rpc-server-x86_64-macos
|
||||||
path: deltachat-rpc-server-x86_64-macos.d
|
path: deltachat-rpc-server-x86_64-macos.d
|
||||||
|
|
||||||
- name: Download macOS binary for aarch64
|
- name: Download macOS binary for aarch64
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-aarch64-macos
|
name: deltachat-rpc-server-aarch64-macos
|
||||||
path: deltachat-rpc-server-aarch64-macos.d
|
path: deltachat-rpc-server-aarch64-macos.d
|
||||||
|
|
||||||
- name: Download Android binary for arm64-v8a
|
- name: Download Android binary for arm64-v8a
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-arm64-v8a-android
|
name: deltachat-rpc-server-arm64-v8a-android
|
||||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||||
|
|
||||||
- name: Download Android binary for armeabi-v7a
|
- name: Download Android binary for armeabi-v7a
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armeabi-v7a-android
|
name: deltachat-rpc-server-armeabi-v7a-android
|
||||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||||
@@ -496,7 +384,7 @@ jobs:
|
|||||||
ls -lah
|
ls -lah
|
||||||
|
|
||||||
- name: Upload to artifacts
|
- name: Upload to artifacts
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-npm-package
|
name: deltachat-rpc-server-npm-package
|
||||||
path: deltachat-rpc-server/npm-package/*.tgz
|
path: deltachat-rpc-server/npm-package/*.tgz
|
||||||
@@ -513,19 +401,16 @@ jobs:
|
|||||||
deltachat-rpc-server/npm-package/*.tgz
|
deltachat-rpc-server/npm-package/*.tgz
|
||||||
|
|
||||||
# Configure Node.js for publishing.
|
# Configure Node.js for publishing.
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
# 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'
|
||||||
working-directory: deltachat-rpc-server/npm-package
|
working-directory: deltachat-rpc-server/npm-package
|
||||||
run: |
|
run: |
|
||||||
ls -lah platform_package
|
ls -lah platform_package
|
||||||
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
|
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|||||||
4
.github/workflows/dependabot.yml
vendored
4
.github/workflows/dependabot.yml
vendored
@@ -10,11 +10,11 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
dependabot:
|
dependabot:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == github.event.pull_request.head.repo.full_name
|
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Dependabot metadata
|
- name: Dependabot metadata
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: dependabot/fetch-metadata@v3.0.0
|
uses: dependabot/fetch-metadata@v2.3.0
|
||||||
with:
|
with:
|
||||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
- name: Approve a PR
|
- name: Approve a PR
|
||||||
|
|||||||
23
.github/workflows/dev-version.yml
vendored
23
.github/workflows/dev-version.yml
vendored
@@ -1,23 +0,0 @@
|
|||||||
# Check that PRs are made against the -dev version.
|
|
||||||
#
|
|
||||||
# If this fails, push commit to update the version to -dev to main.
|
|
||||||
|
|
||||||
name: Check for -dev version
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check_dev_version:
|
|
||||||
name: Check that current version ends with -dev
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Run version-checking script
|
|
||||||
run: scripts/check-dev-version.py
|
|
||||||
14
.github/workflows/jsonrpc-client-npm-package.yml
vendored
14
.github/workflows/jsonrpc-client-npm-package.yml
vendored
@@ -10,28 +10,20 @@ jobs:
|
|||||||
pack-module:
|
pack-module:
|
||||||
name: "Publish @deltachat/jsonrpc-client"
|
name: "Publish @deltachat/jsonrpc-client"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment:
|
|
||||||
name: npm-jsonrpc-client
|
|
||||||
url: https://www.npmjs.com/package/@deltachat/jsonrpc-client
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
# 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/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm install --ignore-scripts
|
run: npm install --ignore-scripts
|
||||||
@@ -45,3 +37,5 @@ jobs:
|
|||||||
- name: Publish
|
- name: Publish
|
||||||
working-directory: deltachat-jsonrpc/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|||||||
6
.github/workflows/jsonrpc.yml
vendored
6
.github/workflows/jsonrpc.yml
vendored
@@ -16,16 +16,16 @@ jobs:
|
|||||||
build_and_test:
|
build_and_test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
- name: Add Rust cache
|
- name: Add Rust cache
|
||||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: npm install
|
- name: npm install
|
||||||
working-directory: deltachat-jsonrpc/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|||||||
27
.github/workflows/nix.yml
vendored
27
.github/workflows/nix.yml
vendored
@@ -5,12 +5,10 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- flake.nix
|
- flake.nix
|
||||||
- flake.lock
|
- flake.lock
|
||||||
- .github/workflows/nix.yml
|
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- flake.nix
|
- flake.nix
|
||||||
- flake.lock
|
- flake.lock
|
||||||
- .github/workflows/nix.yml
|
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
@@ -21,12 +19,15 @@ jobs:
|
|||||||
name: check flake formatting
|
name: check flake formatting
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- run: nix fmt flake.nix -- --check
|
- run: nix fmt
|
||||||
|
|
||||||
|
# Check that formatting does not change anything.
|
||||||
|
- run: git diff --exit-code
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: nix build
|
name: nix build
|
||||||
@@ -80,11 +81,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@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- run: nix build .#${{ matrix.installable }}
|
- run: nix build .#${{ matrix.installable }}
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
@@ -94,16 +95,14 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
installable:
|
installable:
|
||||||
- deltachat-rpc-server
|
- deltachat-rpc-server-aarch64-darwin
|
||||||
- deltachat-rpc-server-x86_64-darwin
|
|
||||||
|
|
||||||
# Fails to build
|
# Fails to bulid
|
||||||
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
|
# - deltachat-rpc-server-x86_64-darwin
|
||||||
# - deltachat-rpc-server-aarch64-darwin
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- 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@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
working-directory: deltachat-rpc-client
|
working-directory: deltachat-rpc-client
|
||||||
run: python3 -m build
|
run: python3 -m build
|
||||||
- name: Store the distribution packages
|
- name: Store the distribution packages
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: python-package-distributions
|
name: python-package-distributions
|
||||||
path: deltachat-rpc-client/dist/
|
path: deltachat-rpc-client/dist/
|
||||||
@@ -42,9 +42,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download all the dists
|
- name: Download all the dists
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
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@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
|||||||
6
.github/workflows/repl.yml
vendored
6
.github/workflows/repl.yml
vendored
@@ -14,15 +14,15 @@ jobs:
|
|||||||
name: Build REPL example
|
name: Build REPL example
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: nix build .#deltachat-repl-win64
|
run: nix build .#deltachat-repl-win64
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: repl.exe
|
name: repl.exe
|
||||||
path: "result/bin/deltachat-repl.exe"
|
path: "result/bin/deltachat-repl.exe"
|
||||||
|
|||||||
67
.github/workflows/upload-docs.yml
vendored
67
.github/workflows/upload-docs.yml
vendored
@@ -1,21 +1,19 @@
|
|||||||
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, py.delta.chat and cffi.delta.chat
|
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- build_jsonrpc_docs_ci
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-rs:
|
build-rs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment:
|
|
||||||
name: rs.delta.chat
|
|
||||||
url: https://rs.delta.chat/
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -25,72 +23,62 @@ jobs:
|
|||||||
- name: Upload to rs.delta.chat
|
- name: Upload to rs.delta.chat
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.RS_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.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:/var/www/html/rs.delta.chat/"
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.USERNAME }}@rs.delta.chat:/var/www/html/rs/"
|
||||||
|
|
||||||
build-python:
|
build-python:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment:
|
|
||||||
name: py.delta.chat
|
|
||||||
url: https://py.delta.chat/
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
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@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- 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
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.PY_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.CODESPEAK_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:/var/www/html/py.delta.chat"
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@py.delta.chat:/home/delta/build/master"
|
||||||
|
|
||||||
build-c:
|
build-c:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment:
|
|
||||||
name: c.delta.chat
|
|
||||||
url: https://c.delta.chat/
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
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@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- 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
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.C_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.CODESPEAK_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:/var/www/html/c.delta.chat"
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
|
||||||
|
|
||||||
build-ts:
|
build-ts:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment:
|
|
||||||
name: js.jsonrpc.delta.chat
|
|
||||||
url: https://js.jsonrpc.delta.chat/
|
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./deltachat-jsonrpc/typescript
|
working-directory: ./deltachat-jsonrpc/typescript
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
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@v6
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
- name: npm install
|
- name: npm install
|
||||||
@@ -102,27 +90,6 @@ jobs:
|
|||||||
- name: Upload to js.jsonrpc.delta.chat
|
- name: Upload to js.jsonrpc.delta.chat
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.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/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:/var/www/html/js.jsonrpc.delta.chat/"
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.USERNAME }}@js.jsonrpc.delta.chat:/var/www/html/js-jsonrpc/"
|
||||||
|
|
||||||
build-cffi:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment:
|
|
||||||
name: cffi.delta.chat
|
|
||||||
url: https://cffi.delta.chat/
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Build the documentation with cargo
|
|
||||||
run: |
|
|
||||||
cargo doc --package deltachat_ffi --no-deps
|
|
||||||
- name: Upload to cffi.delta.chat
|
|
||||||
run: |
|
|
||||||
mkdir -p "$HOME/.ssh"
|
|
||||||
echo "${{ secrets.CFFI_DOCS_SSH_KEY }}" > "$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:/var/www/html/cffi.delta.chat/"
|
|
||||||
|
|||||||
31
.github/workflows/upload-ffi-docs.yml
vendored
Normal file
31
.github/workflows/upload-ffi-docs.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# GitHub Actions workflow
|
||||||
|
# to build `deltachat_ffi` crate documentation
|
||||||
|
# and upload it to <https://cffi.delta.chat/>
|
||||||
|
|
||||||
|
name: Build & Deploy Documentation on cffi.delta.chat
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Build the documentation with cargo
|
||||||
|
run: |
|
||||||
|
cargo doc --package deltachat_ffi --no-deps
|
||||||
|
- name: Upload to cffi.delta.chat
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/.ssh"
|
||||||
|
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
||||||
|
chmod 600 "$HOME/.ssh/key"
|
||||||
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.USERNAME }}@delta.chat:/var/www/html/cffi/"
|
||||||
21
.github/workflows/zizmor-scan.yml
vendored
21
.github/workflows/zizmor-scan.yml
vendored
@@ -6,21 +6,26 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: ["**"]
|
branches: ["**"]
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
zizmor:
|
zizmor:
|
||||||
name: Run zizmor
|
name: zizmor latest via PyPI
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
security-events: write
|
||||||
contents: read
|
|
||||||
actions: read
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Install the latest version of uv
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
|
|
||||||
- name: Run zizmor
|
- name: Run zizmor
|
||||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
run: uvx zizmor --format sarif . > results.sarif
|
||||||
|
|
||||||
|
- name: Upload SARIF file
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
with:
|
||||||
|
sarif_file: results.sarif
|
||||||
|
category: zizmor
|
||||||
|
|||||||
6
.github/zizmor.yml
vendored
6
.github/zizmor.yml
vendored
@@ -1,6 +0,0 @@
|
|||||||
rules:
|
|
||||||
unpinned-uses:
|
|
||||||
config:
|
|
||||||
policies:
|
|
||||||
actions/*: ref-pin
|
|
||||||
dependabot/*: ref-pin
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -36,7 +36,6 @@ deltachat-ffi/xml
|
|||||||
coverage/
|
coverage/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
.vscode
|
||||||
.zed
|
|
||||||
python/accounts.txt
|
python/accounts.txt
|
||||||
python/all-testaccounts.txt
|
python/all-testaccounts.txt
|
||||||
tmp/
|
tmp/
|
||||||
@@ -54,4 +53,3 @@ result
|
|||||||
# direnv
|
# direnv
|
||||||
.envrc
|
.envrc
|
||||||
.direnv
|
.direnv
|
||||||
.aider*
|
|
||||||
|
|||||||
1850
CHANGELOG.md
1850
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
# Contributing to chatmail core
|
# Contributing to Delta Chat
|
||||||
|
|
||||||
## Bug reports
|
## Bug reports
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ If you want to contribute a code, follow this guide.
|
|||||||
|
|
||||||
The following prefix types are used:
|
The following prefix types are used:
|
||||||
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
|
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
|
||||||
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is canceled"
|
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
|
||||||
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
|
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
|
||||||
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
|
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
|
||||||
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
|
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
|
||||||
|
|||||||
2425
Cargo.lock
generated
2425
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
91
Cargo.toml
91
Cargo.toml
@@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "2.50.0-dev"
|
version = "1.159.3"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
rust-version = "1.89"
|
rust-version = "1.82"
|
||||||
repository = "https://github.com/chatmail/core"
|
repository = "https://github.com/chatmail/core"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
@@ -18,9 +18,6 @@ opt-level = 1
|
|||||||
debug = 1
|
debug = 1
|
||||||
opt-level = 0
|
opt-level = 0
|
||||||
|
|
||||||
[profile.fuzz]
|
|
||||||
inherits = "test"
|
|
||||||
|
|
||||||
# Always optimize dependencies.
|
# Always optimize dependencies.
|
||||||
# This does not apply to crates in the workspace.
|
# This does not apply to crates in the workspace.
|
||||||
# <https://doc.rust-lang.org/cargo/reference/profiles.html#overrides>
|
# <https://doc.rust-lang.org/cargo/reference/profiles.html#overrides>
|
||||||
@@ -44,78 +41,78 @@ 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.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
|
async-imap = { version = "0.10.4", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||||
async-native-tls = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
|
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
async-smtp = { version = "0.10", default-features = false, features = ["runtime-tokio"] }
|
||||||
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
|
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
blake3 = "1.8.2"
|
|
||||||
brotli = { version = "8", default-features=false, features = ["std"] }
|
brotli = { version = "8", default-features=false, features = ["std"] }
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||||
colorutils-rs = { version = "0.8.0", default-features = false }
|
|
||||||
data-encoding = "2.9.0"
|
data-encoding = "2.9.0"
|
||||||
escaper = "0.1"
|
escaper = "0.1"
|
||||||
fast-socks5 = "1"
|
fast-socks5 = "0.10"
|
||||||
fd-lock = "4"
|
fd-lock = "4"
|
||||||
futures-lite = { workspace = true }
|
futures-lite = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
hex = "0.4.0"
|
hex = "0.4.0"
|
||||||
|
hickory-resolver = "=0.25.0-alpha.5"
|
||||||
http-body-util = "0.1.3"
|
http-body-util = "0.1.3"
|
||||||
humansize = "2"
|
humansize = "2"
|
||||||
hyper = "1"
|
hyper = "1"
|
||||||
hyper-util = "0.1.16"
|
hyper-util = "0.1.11"
|
||||||
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||||
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
|
iroh-gossip = { version = "0.33", default-features = false, features = ["net"] }
|
||||||
iroh = { version = "0.35", default-features = false }
|
iroh = { version = "0.33", default-features = false }
|
||||||
kamadak-exif = "0.6.1"
|
kamadak-exif = "0.6.1"
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
mail-builder = { version = "0.4.4", default-features = false }
|
mail-builder = { version = "0.4.2", default-features = false }
|
||||||
mailparse = { workspace = true }
|
mailparse = { workspace = true }
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
num_cpus = "1.17"
|
num_cpus = "1.16"
|
||||||
num-derive = "0.4"
|
num-derive = "0.4"
|
||||||
num-traits = { workspace = true }
|
num-traits = { workspace = true }
|
||||||
parking_lot = "0.12.4"
|
parking_lot = "0.12"
|
||||||
percent-encoding = "2.3"
|
percent-encoding = "2.3"
|
||||||
pgp = { version = "0.19.0", default-features = false }
|
pgp = { version = "0.15.0", default-features = false }
|
||||||
pin-project = "1"
|
pin-project = "1"
|
||||||
qrcodegen = "1.7.0"
|
qrcodegen = "1.7.0"
|
||||||
quick-xml = { version = "0.39", features = ["escape-html"] }
|
quick-xml = "0.37"
|
||||||
rand-old = { package = "rand", version = "0.8" }
|
quoted_printable = "0.5"
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||||
|
rust-hsluv = "0.1"
|
||||||
|
rustls-pki-types = "1.11.0"
|
||||||
|
rustls = { version = "0.23.22", default-features = false }
|
||||||
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_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"
|
||||||
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
||||||
smallvec = "1.15.1"
|
smallvec = "1.15.0"
|
||||||
strum = "0.28"
|
strum = "0.27"
|
||||||
strum_macros = "0.28"
|
strum_macros = "0.27"
|
||||||
tagger = "4.3.4"
|
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.0"
|
||||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||||
astral-tokio-tar = { version = "0.6.1", default-features = false }
|
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||||
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.8"
|
||||||
tracing = "0.1.41"
|
|
||||||
url = "2"
|
url = "2"
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
walkdir = "2.5.0"
|
|
||||||
webpki-roots = "0.26.8"
|
webpki-roots = "0.26.8"
|
||||||
|
blake3 = "1.8.2"
|
||||||
|
|
||||||
[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.5.1", features = ["async_tokio"] }
|
||||||
futures-lite = { workspace = true }
|
futures-lite = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
@@ -156,11 +153,6 @@ name = "receive_emails"
|
|||||||
required-features = ["internals"]
|
required-features = ["internals"]
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "decrypting"
|
|
||||||
required-features = ["internals"]
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "get_chat_msgs"
|
name = "get_chat_msgs"
|
||||||
harness = false
|
harness = false
|
||||||
@@ -179,29 +171,29 @@ harness = false
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
async-channel = "2.5.0"
|
async-channel = "2.3.1"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
chrono = { version = "0.4.44", default-features = false }
|
chrono = { version = "0.4.41", 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 }
|
||||||
deltachat = { path = ".", default-features = false }
|
deltachat = { path = ".", default-features = false }
|
||||||
futures = "0.3.32"
|
futures = "0.3.31"
|
||||||
futures-lite = "2.6.1"
|
futures-lite = "2.6.0"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mailparse = "0.16.1"
|
mailparse = "0.16.1"
|
||||||
nu-ansi-term = "0.50"
|
nu-ansi-term = "0.46"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
rand = "0.9"
|
rand = "0.8"
|
||||||
regex = "1.12"
|
regex = "1.10"
|
||||||
rusqlite = "0.37"
|
rusqlite = "0.32"
|
||||||
sanitize-filename = "0.6"
|
sanitize-filename = "0.5"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tempfile = "3.27.0"
|
tempfile = "3.19.1"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = "1"
|
tokio = "1"
|
||||||
tokio-util = "0.7.18"
|
tokio-util = "0.7.14"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
yerpc = "0.6.4"
|
yerpc = "0.6.4"
|
||||||
|
|
||||||
@@ -209,8 +201,7 @@ yerpc = "0.6.4"
|
|||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
internals = []
|
internals = []
|
||||||
vendored = [
|
vendored = [
|
||||||
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
"rusqlite/bundled-sqlcipher-vendored-openssl"
|
||||||
"async-native-tls/vendored"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
|
|||||||
44
README.md
44
README.md
@@ -80,41 +80,30 @@ Connect to your mail server (if already configured):
|
|||||||
> connect
|
> connect
|
||||||
```
|
```
|
||||||
|
|
||||||
Export your public key to a vCard file:
|
Create a contact:
|
||||||
|
|
||||||
```
|
|
||||||
> make-vcard my.vcard 1
|
|
||||||
```
|
|
||||||
|
|
||||||
Create contacts by address or vCard file:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
> addcontact yourfriends@email.org
|
> addcontact yourfriends@email.org
|
||||||
> import-vcard key-contact.vcard
|
Command executed successfully.
|
||||||
```
|
```
|
||||||
|
|
||||||
List contacts:
|
List contacts:
|
||||||
|
|
||||||
```
|
```
|
||||||
> listcontacts
|
> listcontacts
|
||||||
Contact#Contact#11: key-contact@email.org <key-contact@email.org>
|
Contact#10: <name unset> <yourfriends@email.org>
|
||||||
Contact#Contact#Self: Me √ <your@email.org>
|
Contact#1: Me √√ <your@email.org>
|
||||||
2 key contacts.
|
|
||||||
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
|
|
||||||
1 address contacts.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a chat with your friend and send a message:
|
Create a chat with your friend and send a message:
|
||||||
|
|
||||||
```
|
```
|
||||||
> createchat 10
|
> createchat 10
|
||||||
Single#Chat#12 created successfully.
|
Single#10 created successfully.
|
||||||
> chat 12
|
> chat 10
|
||||||
Selecting chat Chat#12
|
Single#10: yourfriends@email.org [yourfriends@email.org]
|
||||||
Single#Chat#12: yourfriends@email.org [yourfriends@email.org] Icon: profile-db-blobs/4138c52e5bc1c576cda7dd44d088c07.png
|
|
||||||
0 messages.
|
|
||||||
81.252µs to create this list, 123.625µs to mark all messages as noticed.
|
|
||||||
> send hi
|
> send hi
|
||||||
|
Message sent.
|
||||||
```
|
```
|
||||||
|
|
||||||
List messages when inside a chat:
|
List messages when inside a chat:
|
||||||
@@ -167,13 +156,13 @@ $ 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 cargo-bolero
|
$ cargo install cargo-bolero@0.8.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Run fuzzing tests with
|
Run fuzzing tests with
|
||||||
```sh
|
```sh
|
||||||
$ cd fuzz
|
$ cd fuzz
|
||||||
$ cargo bolero test fuzz_mailparse -s NONE
|
$ cargo bolero test fuzz_mailparse --release=false -s NONE
|
||||||
```
|
```
|
||||||
|
|
||||||
Corpus is created at `fuzz/fuzz_targets/corpus`,
|
Corpus is created at `fuzz/fuzz_targets/corpus`,
|
||||||
@@ -181,6 +170,11 @@ you can add initial inputs there.
|
|||||||
For `fuzz_mailparse` target corpus can be populated with
|
For `fuzz_mailparse` target corpus can be populated with
|
||||||
`../test-data/message/*.eml`.
|
`../test-data/message/*.eml`.
|
||||||
|
|
||||||
|
To run with AFL instead of libFuzzer:
|
||||||
|
```sh
|
||||||
|
$ cargo bolero test fuzz_format_flowed --release=false -e afl -s NONE
|
||||||
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
||||||
@@ -197,10 +191,12 @@ and then run the script.
|
|||||||
Language bindings are available for:
|
Language bindings are available for:
|
||||||
|
|
||||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||||
- -> libdeltachat is going to be deprecated and only exists because Android, iOS and Ubuntu Touch are still using it. If you build a new project, then please use the jsonrpc api instead.
|
|
||||||
- **JS**: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\]
|
- **JS**: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\]
|
||||||
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
||||||
- **Go** \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
- **Go**
|
||||||
|
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
||||||
|
- over cffi[^1]: \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
|
||||||
|
- **Free Pascal**[^1] \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
|
||||||
- **Java** and **Swift** (contained in the Android/iOS repos)
|
- **Java** and **Swift** (contained in the Android/iOS repos)
|
||||||
|
|
||||||
The following "frontend" projects make use of the Rust-library
|
The following "frontend" projects make use of the Rust-library
|
||||||
@@ -213,3 +209,5 @@ or its language bindings:
|
|||||||
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
||||||
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
|
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
|
||||||
- several **Bots**
|
- several **Bots**
|
||||||
|
|
||||||
|
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.
|
||||||
|
|||||||
55
RELEASE.md
55
RELEASE.md
@@ -1,4 +1,4 @@
|
|||||||
# Releasing a new version of chatmail core
|
# Releasing a new version of DeltaChat core
|
||||||
|
|
||||||
For example, to release version 1.116.0 of the core, do the following steps.
|
For example, to release version 1.116.0 of the core, do the following steps.
|
||||||
|
|
||||||
@@ -14,55 +14,8 @@ For example, to release version 1.116.0 of the core, do the following steps.
|
|||||||
5. Commit the changes as `chore(release): prepare for 1.116.0`.
|
5. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||||
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
||||||
|
|
||||||
6. Push the commit to the `main` branch.
|
6. Tag the release: `git tag --annotate v1.116.0`.
|
||||||
|
|
||||||
7. Once the commit is on the `main` branch and passed CI, tag the release: `git tag --annotate v1.116.0`.
|
7. Push the release tag: `git push origin v1.116.0`.
|
||||||
|
|
||||||
8. Push the release tag: `git push origin v1.116.0`.
|
8. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
|
||||||
|
|
||||||
9. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
|
|
||||||
|
|
||||||
10. Update the version to the next development version:
|
|
||||||
`scripts/set_core_version.py 1.117.0-dev`.
|
|
||||||
|
|
||||||
11. Commit and push the change:
|
|
||||||
`git commit -m "chore: bump version to 1.117.0-dev" && git push origin main`.
|
|
||||||
|
|
||||||
12. Once the binaries are generated and [published](https://github.com/chatmail/core/releases),
|
|
||||||
check Windows binaries for false positive detections at [VirusTotal].
|
|
||||||
Either upload the binaries directly or submit a direct link to the artifact.
|
|
||||||
You can use [old browsers interface](https://www.virustotal.com/old-browsers/)
|
|
||||||
if there are problems with using the default website.
|
|
||||||
If you submit a direct link and get to the page saying
|
|
||||||
"No security vendors flagged this URL as malicious",
|
|
||||||
it does not mean that the file itself is not detected.
|
|
||||||
You need to go to the "details" tab
|
|
||||||
and click on the SHA-256 hash in the "Body SHA-256" section.
|
|
||||||
If any false positive is detected,
|
|
||||||
open an issue to track removing it.
|
|
||||||
See <https://github.com/chatmail/core/issues/7847>
|
|
||||||
for an example of false positive detection issue.
|
|
||||||
If there is a false positive "Microsoft" detection,
|
|
||||||
mark the issue as a blocker.
|
|
||||||
|
|
||||||
[VirusTotal]: https://www.virustotal.com/
|
|
||||||
|
|
||||||
## Dealing with antivirus false positives
|
|
||||||
|
|
||||||
If Windows release is incorrectly detected by some antivirus, submit requests to remove detection.
|
|
||||||
|
|
||||||
"Microsoft" antivirus is built in Windows and will break user setups so removing its detection should be highest priority.
|
|
||||||
To submit false positive to Microsoft, go to <https://www.microsoft.com/en-us/wdsi/filesubmission> and select "Submit file as a ... Software developer" option.
|
|
||||||
|
|
||||||
False positive contacts for other vendors can be found at <https://docs.virustotal.com/docs/false-positive-contacts>.
|
|
||||||
Not all of them may be up to date, so check the links below first.
|
|
||||||
Previously we successfully used the following contacts:
|
|
||||||
- [ESET-NOD32](mailto:samples@eset.com)
|
|
||||||
- [Symantec](https://symsubmit.symantec.com/)
|
|
||||||
|
|
||||||
## Dealing with failed releases
|
|
||||||
|
|
||||||
Once you make a GitHub release,
|
|
||||||
CI will try to build and publish [PyPI](https://pypi.org/) and [npm](https://www.npmjs.com/) packages.
|
|
||||||
If this fails for some reason, do not modify the failed tag, do not delete it and do not force-push to the `main` branch.
|
|
||||||
Fix the build process and tag a new release instead.
|
|
||||||
|
|||||||
84
STYLE.md
84
STYLE.md
@@ -16,12 +16,11 @@ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||||||
text TEXT DEFAULT '' NOT NULL -- message text
|
text TEXT DEFAULT '' NOT NULL -- message text
|
||||||
) STRICT",
|
) STRICT",
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.context("CREATE TABLE messages")?;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
|
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
|
||||||
or [`indoc!`](https://docs.rs/indoc).
|
or [`indoc!](https://docs.rs/indoc).
|
||||||
Do not escape newlines like this:
|
Do not escape newlines like this:
|
||||||
```
|
```
|
||||||
sql.execute(
|
sql.execute(
|
||||||
@@ -30,8 +29,7 @@ id INTEGER PRIMARY KEY AUTOINCREMENT, \
|
|||||||
text TEXT DEFAULT '' NOT NULL \
|
text TEXT DEFAULT '' NOT NULL \
|
||||||
) STRICT",
|
) STRICT",
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.context("CREATE TABLE messages")?;
|
|
||||||
```
|
```
|
||||||
Escaping newlines
|
Escaping newlines
|
||||||
is prone to errors like this if space before backslash is missing:
|
is prone to errors like this if space before backslash is missing:
|
||||||
@@ -65,15 +63,6 @@ an older version. Also don't change the column type, consider adding a new colum
|
|||||||
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
|
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
|
||||||
keyword doesn't help here.
|
keyword doesn't help here.
|
||||||
|
|
||||||
Consider adding context to `anyhow` errors for SQL statements using `.context()` so that it's
|
|
||||||
possible to understand from logs which statement failed. See [Errors](#errors) for more info.
|
|
||||||
|
|
||||||
When changing complex SQL queries, test them on a new database with `EXPLAIN QUERY PLAN`
|
|
||||||
to make sure that indexes are used and large tables are not going to be scanned.
|
|
||||||
Never run `ANALYZE` on the databases,
|
|
||||||
this makes query planner unpredictable
|
|
||||||
and may make performance significantly worse: <https://github.com/chatmail/core/issues/6585>
|
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
||||||
@@ -89,52 +78,12 @@ All errors should be handled in one of these ways:
|
|||||||
- With `.log_err().ok()`.
|
- With `.log_err().ok()`.
|
||||||
- Bubbled up with `?`.
|
- Bubbled up with `?`.
|
||||||
|
|
||||||
When working with [async streams](https://docs.rs/futures/0.3.31/futures/stream/index.html),
|
|
||||||
prefer [`try_next`](https://docs.rs/futures/0.3.31/futures/stream/trait.TryStreamExt.html#method.try_next) over `next()`, e.g. do
|
|
||||||
```
|
|
||||||
while let Some(event) = stream.try_next().await? {
|
|
||||||
todo!();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
instead of
|
|
||||||
```
|
|
||||||
while let Some(event_res) = stream.next().await {
|
|
||||||
todo!();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
as it allows bubbling up the error early with `?`
|
|
||||||
with no way to accidentally skip error processing
|
|
||||||
with early `continue` or `break`.
|
|
||||||
Some streams reading from a connection
|
|
||||||
return infinite number of `Some(Err(_))`
|
|
||||||
items when connection breaks and not processing
|
|
||||||
errors may result in infinite loop.
|
|
||||||
|
|
||||||
`backtrace` feature is enabled for `anyhow` crate
|
`backtrace` feature is enabled for `anyhow` crate
|
||||||
and `debug = 1` option is set in the test profile.
|
and `debug = 1` option is set in the test profile.
|
||||||
This allows to run `RUST_BACKTRACE=1 cargo test`
|
This allows to run `RUST_BACKTRACE=1 cargo test`
|
||||||
and get a backtrace with line numbers in resultified tests
|
and get a backtrace with line numbers in resultified tests
|
||||||
which return `anyhow::Result`.
|
which return `anyhow::Result`.
|
||||||
|
|
||||||
`unwrap` and `expect` are not used in the library
|
|
||||||
because panics are difficult to debug on user devices.
|
|
||||||
However, in the tests `.expect` may be used.
|
|
||||||
Follow
|
|
||||||
<https://doc.rust-lang.org/core/error/index.html#common-message-styles>
|
|
||||||
for `.expect` message style.
|
|
||||||
|
|
||||||
## BTreeMap vs HashMap
|
|
||||||
|
|
||||||
Prefer [BTreeMap](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html)
|
|
||||||
over [HashMap](https://doc.rust-lang.org/std/collections/struct.HashMap.html)
|
|
||||||
and [BTreeSet](https://doc.rust-lang.org/std/collections/struct.BTreeSet.html)
|
|
||||||
over [HashSet](https://doc.rust-lang.org/std/collections/struct.HashSet.html)
|
|
||||||
as iterating over these structures returns items in deterministic order.
|
|
||||||
|
|
||||||
Non-deterministic code may result in difficult to reproduce bugs,
|
|
||||||
flaky tests, regression tests that miss bugs
|
|
||||||
or different behavior on different devices when processing the same messages.
|
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
For logging, use `info!`, `warn!` and `error!` macros.
|
For logging, use `info!`, `warn!` and `error!` macros.
|
||||||
@@ -147,30 +96,3 @@ Format anyhow errors with `{:#}` to print all the contexts like this:
|
|||||||
```
|
```
|
||||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation comments
|
|
||||||
|
|
||||||
All public modules, methods and fields should be documented.
|
|
||||||
This is checked by [`missing_docs`](https://doc.rust-lang.org/rustdoc/lints.html#missing_docs) lint.
|
|
||||||
|
|
||||||
Private items do not have to be documented,
|
|
||||||
but CI uses `cargo doc --document-private-items`
|
|
||||||
to build the documentation,
|
|
||||||
so it is preferred that new items
|
|
||||||
are documented.
|
|
||||||
|
|
||||||
Follow Rust guidelines for the documentation comments:
|
|
||||||
<https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#summary-sentence>
|
|
||||||
|
|
||||||
## Do not use `into()`, `try_into()` or `parse()`
|
|
||||||
|
|
||||||
For internal types, implementing `From`, `TryFrom` or `FromStr` is discouraged.
|
|
||||||
Instead, a `new()` function is recommended.
|
|
||||||
|
|
||||||
For external types, prefer using `Type::from()`, `Type::try_from()` or `Type::from_str()`
|
|
||||||
over `into()`, `try_into()` or `parse()`.
|
|
||||||
|
|
||||||
Calling `into()`, `try_into()` or `parse()`
|
|
||||||
creates an indirection,
|
|
||||||
which is hard to follow for people who are not familiar with Rust,
|
|
||||||
or who are not using rust-analyzer.
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1,47 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
height="480"
|
|
||||||
viewBox="0 -960 9600 9600"
|
|
||||||
width="480"
|
|
||||||
fill="#ffffff"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
sodipodi:docname="icon-email.svg"
|
|
||||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs1" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:zoom="0.99091847"
|
|
||||||
inkscape:cx="263.392"
|
|
||||||
inkscape:cy="177.613"
|
|
||||||
inkscape:window-width="1884"
|
|
||||||
inkscape:window-height="1052"
|
|
||||||
inkscape:window-x="36"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg1" />
|
|
||||||
<rect
|
|
||||||
style="fill:#8c8c8c;fill-opacity:1;stroke:none;stroke-width:680.523;stroke-dasharray:none;paint-order:markers fill stroke"
|
|
||||||
id="rect1"
|
|
||||||
width="9951.9541"
|
|
||||||
height="9767.4756"
|
|
||||||
x="-71.697792"
|
|
||||||
y="-1012.83"
|
|
||||||
ry="0.43547946" />
|
|
||||||
<path
|
|
||||||
d="m 2948.0033,5553.6941 q -130.7292,0 -228.7761,-96.3953 -98.0468,-96.3953 -98.0468,-224.9223 V 2447.6234 q 0,-128.527 98.0468,-224.9223 98.0469,-96.3953 228.7761,-96.3953 h 3703.9934 q 130.7292,0 228.776,96.3953 98.0469,96.3953 98.0469,224.9223 v 2784.7531 q 0,128.527 -98.0469,224.9223 -98.0468,96.3953 -228.776,96.3953 z M 4800,3936.3952 2948.0033,2742.1646 V 5232.3765 H 6651.9967 V 2742.1646 Z m 0,-321.3176 1830.2085,-1167.4541 h -3654.97 z m -1851.9967,-872.913 v -294.5412 2784.7531 z"
|
|
||||||
id="path1"
|
|
||||||
style="stroke-width:5.40098" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,7 +0,0 @@
|
|||||||
BEGIN:VCARD
|
|
||||||
VERSION:4.0
|
|
||||||
EMAIL:self_reporting@testrun.org
|
|
||||||
FN:Statistics bot
|
|
||||||
KEY:data:application/pgp-keys;base64,xjMEZbfBlBYJKwYBBAHaRw8BAQdABpLWS2PUIGGo4pslVt4R8sylP5wZihmhf1DTDr3oCMPNHDxzZWxmX3JlcG9ydGluZ0B0ZXN0cnVuLm9yZz7CiwQQFggAMwIZAQUCZbfBlAIbAwQLCQgHBhUICQoLAgMWAgEWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohD8dAQCQV7CoH6UP4PD+NqI4kW5tbbqdh2AnDROg60qotmLExAEAxDfd3QHAK9f8b9qQUbLmHIztCLxhEuVbWPBEYeVW0gvOOARlt8GUEgorBgEEAZdVAQUBAQdAMBUhYoAAcI625vGZqnM5maPX4sGJ7qvJxPAFILPy6AcDAQgHwngEGBYIACAFAmW3wZQCGwwWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohPwCAQCvzk1ObIkj2GqsuIfaULlgdnfdZY8LNary425CEfHZDQD5AblXVrlMO1frdlc/Vo9z3pEeCrfYdD7ITD3/OeVoiQ4=
|
|
||||||
REV:20250412T195751Z
|
|
||||||
END:VCARD
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
use std::hint::black_box;
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
|
||||||
use deltachat::Events;
|
|
||||||
use deltachat::contact::Contact;
|
use deltachat::contact::Contact;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
|
use deltachat::Events;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
async fn address_book_benchmark(n: u32, read_count: u32) {
|
async fn address_book_benchmark(n: u32, read_count: u32) {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
use std::hint::black_box;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
use deltachat::accounts::Accounts;
|
use deltachat::accounts::Accounts;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
//! Benchmarks for message decryption,
|
|
||||||
//! comparing decryption of symmetrically-encrypted messages
|
|
||||||
//! to decryption of asymmetrically-encrypted messages.
|
|
||||||
//!
|
|
||||||
//! Call with
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! cargo bench --bench decrypting --features="internals"
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! or, if you want to only run e.g. the 'Decrypt and parse a symmetrically encrypted message' benchmark:
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt and parse a symmetrically encrypted message'
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! You can also pass a substring:
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! cargo bench --bench decrypting --features="internals" -- 'symmetrically'
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Symmetric decryption has to try out all known secrets,
|
|
||||||
//! You can benchmark this by adapting the `NUM_SECRETS` variable.
|
|
||||||
|
|
||||||
use std::hint::black_box;
|
|
||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
|
||||||
use deltachat::internals_for_benches::create_broadcast_secret;
|
|
||||||
use deltachat::internals_for_benches::save_broadcast_secret;
|
|
||||||
use deltachat::securejoin::get_securejoin_qr;
|
|
||||||
use deltachat::{
|
|
||||||
Events, chat::ChatId, config::Config, context::Context, internals_for_benches::key_from_asc,
|
|
||||||
internals_for_benches::parse_and_get_text, internals_for_benches::store_self_keypair,
|
|
||||||
stock_str::StockStrings,
|
|
||||||
};
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
static NUM_BROADCAST_SECRETS: LazyLock<usize> = LazyLock::new(|| {
|
|
||||||
std::env::var("NUM_BROADCAST_SECRETS")
|
|
||||||
.unwrap_or("500".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
static NUM_AUTH_TOKENS: LazyLock<usize> = LazyLock::new(|| {
|
|
||||||
std::env::var("NUM_AUTH_TOKENS")
|
|
||||||
.unwrap_or("5000".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
async fn create_context() -> Context {
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let dbfile = dir.path().join("db.sqlite");
|
|
||||||
let context = Context::new(dbfile.as_path(), 100, Events::new(), StockStrings::new())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
context
|
|
||||||
.set_config(Config::ConfiguredAddr, Some("bob@example.net"))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let secret = key_from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap();
|
|
||||||
store_self_keypair(&context, &secret)
|
|
||||||
.await
|
|
||||||
.expect("Failed to save key");
|
|
||||||
|
|
||||||
context
|
|
||||||
}
|
|
||||||
|
|
||||||
fn criterion_benchmark(c: &mut Criterion) {
|
|
||||||
let mut group = c.benchmark_group("Decrypt");
|
|
||||||
|
|
||||||
// ===========================================================================================
|
|
||||||
// Benchmarks for the whole parsing pipeline, incl. decryption (but excl. receive_imf())
|
|
||||||
// ===========================================================================================
|
|
||||||
|
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
||||||
let mut secrets = generate_secrets();
|
|
||||||
|
|
||||||
// "secret" is the shared secret that was used to encrypt text_symmetrically_encrypted.eml.
|
|
||||||
// Put it into the middle of our secrets:
|
|
||||||
secrets[*NUM_BROADCAST_SECRETS / 2] = "secret".to_string();
|
|
||||||
|
|
||||||
let context = rt.block_on(async {
|
|
||||||
let context = create_context().await;
|
|
||||||
for (i, secret) in secrets.iter().enumerate() {
|
|
||||||
save_broadcast_secret(&context, ChatId::new(10 + i as u32), secret)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
for _i in 0..*NUM_AUTH_TOKENS {
|
|
||||||
get_securejoin_qr(&context, None).await.unwrap();
|
|
||||||
}
|
|
||||||
println!("NUM_AUTH_TOKENS={}", *NUM_AUTH_TOKENS);
|
|
||||||
context
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_function("Decrypt and parse a symmetrically encrypted message", |b| {
|
|
||||||
b.to_async(&rt).iter(|| {
|
|
||||||
let ctx = context.clone();
|
|
||||||
async move {
|
|
||||||
let text = parse_and_get_text(
|
|
||||||
&ctx,
|
|
||||||
include_bytes!("../test-data/message/text_symmetrically_encrypted.eml"),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(black_box(text), "Symmetrically encrypted message");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_function("Decrypt and parse a public-key encrypted message", |b| {
|
|
||||||
b.to_async(&rt).iter(|| {
|
|
||||||
let ctx = context.clone();
|
|
||||||
async move {
|
|
||||||
let text = parse_and_get_text(
|
|
||||||
&ctx,
|
|
||||||
include_bytes!("../test-data/message/text_from_alice_encrypted.eml"),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(black_box(text), "hi");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_secrets() -> Vec<String> {
|
|
||||||
let secrets: Vec<String> = (0..*NUM_BROADCAST_SECRETS)
|
|
||||||
.map(|_| create_broadcast_secret())
|
|
||||||
.collect();
|
|
||||||
println!("NUM_BROADCAST_SECRETS={}", *NUM_BROADCAST_SECRETS);
|
|
||||||
secrets
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, criterion_benchmark);
|
|
||||||
criterion_main!(benches);
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
use std::hint::black_box;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
use deltachat::Events;
|
|
||||||
use deltachat::chat::{self, ChatId};
|
use deltachat::chat::{self, ChatId};
|
||||||
use deltachat::chatlist::Chatlist;
|
use deltachat::chatlist::Chatlist;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
|
use deltachat::Events;
|
||||||
|
|
||||||
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
|
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
|
||||||
let id = 100;
|
let id = 100;
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
use std::hint::black_box;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
use deltachat::Events;
|
|
||||||
use deltachat::chatlist::Chatlist;
|
use deltachat::chatlist::Chatlist;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
|
use deltachat::Events;
|
||||||
|
|
||||||
async fn get_chat_list_benchmark(context: &Context) {
|
async fn get_chat_list_benchmark(context: &Context) {
|
||||||
Chatlist::try_load(context, 0, None, None).await.unwrap();
|
Chatlist::try_load(context, 0, None, None).await.unwrap();
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
use std::hint::black_box;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
|
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
|
||||||
use deltachat::Events;
|
|
||||||
use deltachat::chat::{self, ChatId};
|
use deltachat::chat::{self, ChatId};
|
||||||
use deltachat::chatlist::Chatlist;
|
use deltachat::chatlist::Chatlist;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
|
use deltachat::Events;
|
||||||
use futures_lite::future::block_on;
|
use futures_lite::future::block_on;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
use std::hint::black_box;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
use deltachat::{
|
use deltachat::{
|
||||||
Events,
|
|
||||||
config::Config,
|
config::Config,
|
||||||
context::Context,
|
context::Context,
|
||||||
imex::{ImexMode, imex},
|
imex::{imex, ImexMode},
|
||||||
receive_imf::receive_imf,
|
receive_imf::receive_imf,
|
||||||
stock_str::StockStrings,
|
stock_str::StockStrings,
|
||||||
|
Events,
|
||||||
};
|
};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
use std::hint::black_box;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
use deltachat::Events;
|
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
|
use deltachat::Events;
|
||||||
|
|
||||||
async fn search_benchmark(dbfile: impl AsRef<Path>) {
|
async fn search_benchmark(dbfile: impl AsRef<Path>) {
|
||||||
let id = 100;
|
let id = 100;
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
use deltachat::{Event, EventType, Events};
|
use deltachat::{info, Event, EventType, Events};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
async fn send_events_benchmark(context: &Context) {
|
async fn send_events_benchmark(context: &Context) {
|
||||||
let emitter = context.get_event_emitter();
|
let emitter = context.get_event_emitter();
|
||||||
for _i in 0..1_000_000 {
|
for _i in 0..1_000_000 {
|
||||||
context.emit_event(EventType::Info("interesting event...".to_string()));
|
info!(context, "interesting event...");
|
||||||
}
|
}
|
||||||
context.emit_event(EventType::Info("DONE".to_string()));
|
info!(context, "DONE");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match emitter.recv().await.unwrap() {
|
match emitter.recv().await.unwrap() {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ body = """
|
|||||||
{% for commit in commits %}
|
{% for commit in commits %}
|
||||||
- {% if commit.breaking %}[**breaking**] {% endif %}\
|
- {% if commit.breaking %}[**breaking**] {% endif %}\
|
||||||
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
|
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
|
||||||
{{ commit.message }}.\
|
{{ commit.message | upper_first }}.\
|
||||||
{% if commit.footers is defined %}\
|
{% if commit.footers is defined %}\
|
||||||
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
|
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
|
||||||
{% raw %} {% endraw %}- {{ footer.value }}\
|
{% raw %} {% endraw %}- {{ footer.value }}\
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ impl ContactAddress {
|
|||||||
pub fn new(s: &str) -> Result<Self> {
|
pub fn new(s: &str) -> Result<Self> {
|
||||||
let addr = addr_normalize(s);
|
let addr = addr_normalize(s);
|
||||||
if !may_be_valid_addr(&addr) {
|
if !may_be_valid_addr(&addr) {
|
||||||
bail!("invalid address {s:?}");
|
bail!("invalid address {:?}", s);
|
||||||
}
|
}
|
||||||
Ok(Self(addr.to_string()))
|
Ok(Self(addr.to_string()))
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ impl ContactAddress {
|
|||||||
|
|
||||||
/// Allow converting [`ContactAddress`] to an SQLite type.
|
/// Allow converting [`ContactAddress`] to an SQLite type.
|
||||||
impl rusqlite::types::ToSql for ContactAddress {
|
impl rusqlite::types::ToSql for ContactAddress {
|
||||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||||
let val = rusqlite::types::Value::Text(self.0.to_string());
|
let val = rusqlite::types::Value::Text(self.0.to_string());
|
||||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||||
Ok(out)
|
Ok(out)
|
||||||
@@ -257,16 +257,16 @@ impl EmailAddress {
|
|||||||
.chars()
|
.chars()
|
||||||
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
||||||
{
|
{
|
||||||
bail!("Email {input:?} must not contain whitespaces, '>' or '<'");
|
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
|
||||||
}
|
}
|
||||||
|
|
||||||
match &parts[..] {
|
match &parts[..] {
|
||||||
[domain, local] => {
|
[domain, local] => {
|
||||||
if local.is_empty() {
|
if local.is_empty() {
|
||||||
bail!("empty string is not valid for local part in {input:?}");
|
bail!("empty string is not valid for local part in {:?}", input);
|
||||||
}
|
}
|
||||||
if domain.is_empty() {
|
if domain.is_empty() {
|
||||||
bail!("missing domain after '@' in {input:?}");
|
bail!("missing domain after '@' in {:?}", input);
|
||||||
}
|
}
|
||||||
if domain.ends_with('.') {
|
if domain.ends_with('.') {
|
||||||
bail!("Domain {domain:?} should not contain the dot in the end");
|
bail!("Domain {domain:?} should not contain the dot in the end");
|
||||||
@@ -276,13 +276,13 @@ impl EmailAddress {
|
|||||||
domain: (*domain).to_string(),
|
domain: (*domain).to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => bail!("Email {input:?} must contain '@' character"),
|
_ => bail!("Email {:?} must contain '@' character", input),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl rusqlite::types::ToSql for EmailAddress {
|
impl rusqlite::types::ToSql for EmailAddress {
|
||||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||||
let val = rusqlite::types::Value::Text(self.to_string());
|
let val = rusqlite::types::Value::Text(self.to_string());
|
||||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||||
Ok(out)
|
Ok(out)
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ pub struct VcardContact {
|
|||||||
pub key: Option<String>,
|
pub key: Option<String>,
|
||||||
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
|
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
|
||||||
pub profile_image: Option<String>,
|
pub profile_image: Option<String>,
|
||||||
/// The biography, stored in the vcard property `note`
|
|
||||||
pub biography: Option<String>,
|
|
||||||
/// The timestamp when the vcard was created / last updated, vcard property `rev`
|
/// The timestamp when the vcard was created / last updated, vcard property `rev`
|
||||||
pub timestamp: Result<i64>,
|
pub timestamp: Result<i64>,
|
||||||
}
|
}
|
||||||
@@ -36,45 +34,6 @@ impl VcardContact {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape(s: &str) -> String {
|
|
||||||
// https://www.rfc-editor.org/rfc/rfc6350.html#section-3.4
|
|
||||||
s
|
|
||||||
// backslash must be first!
|
|
||||||
.replace(r"\", r"\\")
|
|
||||||
.replace(',', r"\,")
|
|
||||||
.replace(';', r"\;")
|
|
||||||
.replace('\n', r"\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unescape(s: &str) -> String {
|
|
||||||
// https://www.rfc-editor.org/rfc/rfc6350.html#section-3.4
|
|
||||||
let mut out = String::new();
|
|
||||||
|
|
||||||
let mut chars = s.chars();
|
|
||||||
while let Some(c) = chars.next() {
|
|
||||||
if c == '\\' {
|
|
||||||
if let Some(next) = chars.next() {
|
|
||||||
match next {
|
|
||||||
'\\' | ',' | ';' => out.push(next),
|
|
||||||
'n' | 'N' => out.push('\n'),
|
|
||||||
_ => {
|
|
||||||
// Invalid escape sequence (keep unchanged)
|
|
||||||
out.push('\\');
|
|
||||||
out.push(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Invalid escape sequence (keep unchanged)
|
|
||||||
out.push('\\');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
out.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a vCard containing given contacts.
|
/// Returns a vCard containing given contacts.
|
||||||
///
|
///
|
||||||
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
|
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
|
||||||
@@ -87,9 +46,8 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
|||||||
|
|
||||||
let mut res = "".to_string();
|
let mut res = "".to_string();
|
||||||
for c in contacts {
|
for c in contacts {
|
||||||
// Mustn't contain ',', but it's easier to escape than to error out.
|
let addr = &c.addr;
|
||||||
let addr = escape(&c.addr);
|
let display_name = c.display_name();
|
||||||
let display_name = escape(c.display_name());
|
|
||||||
res += &format!(
|
res += &format!(
|
||||||
"BEGIN:VCARD\r\n\
|
"BEGIN:VCARD\r\n\
|
||||||
VERSION:4.0\r\n\
|
VERSION:4.0\r\n\
|
||||||
@@ -97,13 +55,10 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
|||||||
FN:{display_name}\r\n"
|
FN:{display_name}\r\n"
|
||||||
);
|
);
|
||||||
if let Some(key) = &c.key {
|
if let Some(key) = &c.key {
|
||||||
res += &format!("KEY:data:application/pgp-keys;base64\\,{key}\r\n");
|
res += &format!("KEY:data:application/pgp-keys;base64,{key}\r\n");
|
||||||
}
|
}
|
||||||
if let Some(profile_image) = &c.profile_image {
|
if let Some(profile_image) = &c.profile_image {
|
||||||
res += &format!("PHOTO:data:image/jpeg;base64\\,{profile_image}\r\n");
|
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
|
||||||
}
|
|
||||||
if let Some(biography) = &c.biography {
|
|
||||||
res += &format!("NOTE:{}\r\n", escape(biography));
|
|
||||||
}
|
}
|
||||||
if let Some(timestamp) = format_timestamp(c) {
|
if let Some(timestamp) = format_timestamp(c) {
|
||||||
res += &format!("REV:{timestamp}\r\n");
|
res += &format!("REV:{timestamp}\r\n");
|
||||||
@@ -124,8 +79,8 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Returns (parameters, raw value) tuple.
|
/// Returns (parameters, value) tuple.
|
||||||
fn vcard_property_raw<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
|
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
|
||||||
let remainder = remove_prefix(line, property)?;
|
let remainder = remove_prefix(line, property)?;
|
||||||
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
|
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
|
||||||
// then `remainder` is now `;TYPE=work:alice@example.com`
|
// then `remainder` is now `;TYPE=work:alice@example.com`
|
||||||
@@ -155,25 +110,23 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
|||||||
}
|
}
|
||||||
Some((params, value))
|
Some((params, value))
|
||||||
}
|
}
|
||||||
/// Returns (parameters, unescaped value) tuple.
|
|
||||||
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
|
|
||||||
let (params, value) = vcard_property_raw(line, property)?;
|
|
||||||
// Some fields can't contain commas, but unescape them everywhere for safety.
|
|
||||||
Some((params, unescape(value)))
|
|
||||||
}
|
|
||||||
fn base64_key(line: &str) -> Option<&str> {
|
fn base64_key(line: &str) -> Option<&str> {
|
||||||
let (params, value) = vcard_property_raw(line, "key")?;
|
let (params, value) = vcard_property(line, "key")?;
|
||||||
if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64")
|
if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64")
|
||||||
|| params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b")
|
|| params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b")
|
||||||
{
|
{
|
||||||
return Some(value);
|
return Some(value);
|
||||||
}
|
}
|
||||||
remove_prefix(value, "data:application/pgp-keys;base64\\,")
|
if let Some(value) = remove_prefix(value, "data:application/pgp-keys;base64,")
|
||||||
// Old Delta Chat format.
|
.or_else(|| remove_prefix(value, r"data:application/pgp-keys;base64\,"))
|
||||||
.or_else(|| remove_prefix(value, "data:application/pgp-keys;base64,"))
|
{
|
||||||
|
return Some(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
fn base64_photo(line: &str) -> Option<&str> {
|
fn base64_photo(line: &str) -> Option<&str> {
|
||||||
let (params, value) = vcard_property_raw(line, "photo")?;
|
let (params, value) = vcard_property(line, "photo")?;
|
||||||
if params.eq_ignore_ascii_case("JPEG;ENCODING=BASE64")
|
if params.eq_ignore_ascii_case("JPEG;ENCODING=BASE64")
|
||||||
|| params.eq_ignore_ascii_case("ENCODING=BASE64;JPEG")
|
|| params.eq_ignore_ascii_case("ENCODING=BASE64;JPEG")
|
||||||
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=b")
|
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=b")
|
||||||
@@ -183,9 +136,13 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
|||||||
{
|
{
|
||||||
return Some(value);
|
return Some(value);
|
||||||
}
|
}
|
||||||
remove_prefix(value, "data:image/jpeg;base64\\,")
|
if let Some(value) = remove_prefix(value, "data:image/jpeg;base64,")
|
||||||
// Old Delta Chat format.
|
.or_else(|| remove_prefix(value, r"data:image/jpeg;base64\,"))
|
||||||
.or_else(|| remove_prefix(value, "data:image/jpeg;base64,"))
|
{
|
||||||
|
return Some(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
fn parse_datetime(datetime: &str) -> Result<i64> {
|
fn parse_datetime(datetime: &str) -> Result<i64> {
|
||||||
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
|
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
|
||||||
@@ -229,7 +186,6 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
|||||||
let mut addr = None;
|
let mut addr = None;
|
||||||
let mut key = None;
|
let mut key = None;
|
||||||
let mut photo = None;
|
let mut photo = None;
|
||||||
let mut biography = None;
|
|
||||||
let mut datetime = None;
|
let mut datetime = None;
|
||||||
|
|
||||||
for mut line in lines.by_ref() {
|
for mut line in lines.by_ref() {
|
||||||
@@ -249,24 +205,18 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
|||||||
key.get_or_insert(k);
|
key.get_or_insert(k);
|
||||||
} else if let Some(p) = base64_photo(line) {
|
} else if let Some(p) = base64_photo(line) {
|
||||||
photo.get_or_insert(p);
|
photo.get_or_insert(p);
|
||||||
} else if let Some((_params, bio)) = vcard_property(line, "note") {
|
|
||||||
biography.get_or_insert(bio);
|
|
||||||
} else if let Some((_params, rev)) = vcard_property(line, "rev") {
|
} else if let Some((_params, rev)) = vcard_property(line, "rev") {
|
||||||
datetime.get_or_insert(rev);
|
datetime.get_or_insert(rev);
|
||||||
} else if line.eq_ignore_ascii_case("END:VCARD") {
|
} else if line.eq_ignore_ascii_case("END:VCARD") {
|
||||||
let (authname, addr) = sanitize_name_and_addr(
|
let (authname, addr) =
|
||||||
&display_name.unwrap_or_default(),
|
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
|
||||||
&addr.unwrap_or_default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
contacts.push(VcardContact {
|
contacts.push(VcardContact {
|
||||||
authname,
|
authname,
|
||||||
addr,
|
addr,
|
||||||
key: key.map(|s| s.to_string()),
|
key: key.map(|s| s.to_string()),
|
||||||
profile_image: photo.map(|s| s.to_string()),
|
profile_image: photo.map(|s| s.to_string()),
|
||||||
biography,
|
|
||||||
timestamp: datetime
|
timestamp: datetime
|
||||||
.as_deref()
|
|
||||||
.context("No timestamp in vcard")
|
.context("No timestamp in vcard")
|
||||||
.and_then(parse_datetime),
|
.and_then(parse_datetime),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ fn test_make_and_parse_vcard() {
|
|||||||
authname: "Alice Wonderland".to_string(),
|
authname: "Alice Wonderland".to_string(),
|
||||||
key: Some("[base64-data]".to_string()),
|
key: Some("[base64-data]".to_string()),
|
||||||
profile_image: Some("image in Base64".to_string()),
|
profile_image: Some("image in Base64".to_string()),
|
||||||
biography: Some("Hi,\nI'm Alice; and this is a backslash: \\".to_string()),
|
|
||||||
timestamp: Ok(1713465762),
|
timestamp: Ok(1713465762),
|
||||||
},
|
},
|
||||||
VcardContact {
|
VcardContact {
|
||||||
@@ -99,7 +98,6 @@ fn test_make_and_parse_vcard() {
|
|||||||
authname: "".to_string(),
|
authname: "".to_string(),
|
||||||
key: None,
|
key: None,
|
||||||
profile_image: None,
|
profile_image: None,
|
||||||
biography: None,
|
|
||||||
timestamp: Ok(0),
|
timestamp: Ok(0),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -108,9 +106,8 @@ fn test_make_and_parse_vcard() {
|
|||||||
VERSION:4.0\r\n\
|
VERSION:4.0\r\n\
|
||||||
EMAIL:alice@example.org\r\n\
|
EMAIL:alice@example.org\r\n\
|
||||||
FN:Alice Wonderland\r\n\
|
FN:Alice Wonderland\r\n\
|
||||||
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
|
KEY:data:application/pgp-keys;base64,[base64-data]\r\n\
|
||||||
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
|
PHOTO:data:image/jpeg;base64,image in Base64\r\n\
|
||||||
NOTE:Hi\\,\\nI'm Alice\\; and this is a backslash: \\\\\r\n\
|
|
||||||
REV:20240418T184242Z\r\n\
|
REV:20240418T184242Z\r\n\
|
||||||
END:VCARD\r\n",
|
END:VCARD\r\n",
|
||||||
"BEGIN:VCARD\r\n\
|
"BEGIN:VCARD\r\n\
|
||||||
@@ -249,8 +246,7 @@ END:VCARD",
|
|||||||
assert_eq!(contacts[0].profile_image, None);
|
assert_eq!(contacts[0].profile_image, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Proton at some point slightly changed the format of their vcards.
|
/// Proton at some point slightly changed the format of their vcards
|
||||||
/// This also tests unescaped commas in PHOTO and KEY (old Delta Chat format).
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_protonmail_vcard2() {
|
fn test_protonmail_vcard2() {
|
||||||
let contacts = parse_vcard(
|
let contacts = parse_vcard(
|
||||||
@@ -276,14 +272,3 @@ END:VCARD",
|
|||||||
assert!(contacts[0].timestamp.is_err());
|
assert!(contacts[0].timestamp.is_err());
|
||||||
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vcard_value_escape_unescape() {
|
|
||||||
let original = "Text, with; chars and a \\ and a newline\nand a literal newline \\n";
|
|
||||||
let expected_escaped = r"Text\, with\; chars and a \\ and a newline\nand a literal newline \\n";
|
|
||||||
|
|
||||||
let escaped = escape(original);
|
|
||||||
assert_eq!(escaped, expected_escaped);
|
|
||||||
let unescaped = unescape(&escaped);
|
|
||||||
assert_eq!(original, unescaped);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "2.50.0-dev"
|
version = "1.159.3"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<doxygenlayout version="1.0">
|
||||||
<doxygenlayout version="2.0">
|
<!-- Generated by doxygen 1.8.20 -->
|
||||||
<!-- Generated by doxygen 1.13.2 -->
|
|
||||||
<!-- Navigation index tabs for HTML output -->
|
<!-- Navigation index tabs for HTML output -->
|
||||||
<navindex>
|
<navindex>
|
||||||
<tab type="mainpage" visible="yes" title=""/>
|
<tab type="mainpage" visible="yes" title=""/>
|
||||||
@@ -12,16 +11,10 @@
|
|||||||
</tab>
|
</tab>
|
||||||
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
||||||
<tab type="pages" visible="yes" title="" intro=""/>
|
<tab type="pages" visible="yes" title="" intro=""/>
|
||||||
<tab type="modules" visible="yes" title="" intro="">
|
|
||||||
<tab type="modulelist" visible="yes" title="" intro=""/>
|
|
||||||
<tab type="modulemembers" visible="yes" title="" intro=""/>
|
|
||||||
</tab>
|
|
||||||
<tab type="namespaces" visible="yes" title="">
|
<tab type="namespaces" visible="yes" title="">
|
||||||
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
||||||
<tab type="namespacemembers" visible="yes" title="" intro=""/>
|
<tab type="namespacemembers" visible="yes" title="" intro=""/>
|
||||||
</tab>
|
</tab>
|
||||||
<tab type="concepts" visible="yes" title="">
|
|
||||||
</tab>
|
|
||||||
<tab type="interfaces" visible="yes" title="">
|
<tab type="interfaces" visible="yes" title="">
|
||||||
<tab type="interfacelist" visible="yes" title="" intro=""/>
|
<tab type="interfacelist" visible="yes" title="" intro=""/>
|
||||||
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||||
@@ -42,228 +35,4 @@
|
|||||||
</tab>
|
</tab>
|
||||||
<tab type="examples" visible="yes" title="" intro=""/>
|
<tab type="examples" visible="yes" title="" intro=""/>
|
||||||
</navindex>
|
</navindex>
|
||||||
|
|
||||||
<!-- Layout definition for a class page -->
|
|
||||||
<class>
|
|
||||||
<briefdescription visible="yes"/>
|
|
||||||
<includes visible="$SHOW_HEADERFILE"/>
|
|
||||||
<inheritancegraph visible="yes"/>
|
|
||||||
<collaborationgraph visible="yes"/>
|
|
||||||
<memberdecl>
|
|
||||||
<nestedclasses visible="yes" title=""/>
|
|
||||||
<publictypes visible="yes" title=""/>
|
|
||||||
<services visible="yes" title=""/>
|
|
||||||
<interfaces visible="yes" title=""/>
|
|
||||||
<publicslots visible="yes" title=""/>
|
|
||||||
<signals visible="yes" title=""/>
|
|
||||||
<publicmethods visible="yes" title=""/>
|
|
||||||
<publicstaticmethods visible="yes" title=""/>
|
|
||||||
<publicattributes visible="yes" title=""/>
|
|
||||||
<publicstaticattributes visible="yes" title=""/>
|
|
||||||
<protectedtypes visible="yes" title=""/>
|
|
||||||
<protectedslots visible="yes" title=""/>
|
|
||||||
<protectedmethods visible="yes" title=""/>
|
|
||||||
<protectedstaticmethods visible="yes" title=""/>
|
|
||||||
<protectedattributes visible="yes" title=""/>
|
|
||||||
<protectedstaticattributes visible="yes" title=""/>
|
|
||||||
<packagetypes visible="yes" title=""/>
|
|
||||||
<packagemethods visible="yes" title=""/>
|
|
||||||
<packagestaticmethods visible="yes" title=""/>
|
|
||||||
<packageattributes visible="yes" title=""/>
|
|
||||||
<packagestaticattributes visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
<events visible="yes" title=""/>
|
|
||||||
<privatetypes visible="yes" title=""/>
|
|
||||||
<privateslots visible="yes" title=""/>
|
|
||||||
<privatemethods visible="yes" title=""/>
|
|
||||||
<privatestaticmethods visible="yes" title=""/>
|
|
||||||
<privateattributes visible="yes" title=""/>
|
|
||||||
<privatestaticattributes visible="yes" title=""/>
|
|
||||||
<friends visible="yes" title=""/>
|
|
||||||
<related visible="yes" title="" subtitle=""/>
|
|
||||||
<membergroups visible="yes"/>
|
|
||||||
</memberdecl>
|
|
||||||
<detaileddescription visible="yes" title=""/>
|
|
||||||
<memberdef>
|
|
||||||
<inlineclasses visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<services visible="yes" title=""/>
|
|
||||||
<interfaces visible="yes" title=""/>
|
|
||||||
<constructors visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<related visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
<events visible="yes" title=""/>
|
|
||||||
</memberdef>
|
|
||||||
<allmemberslink visible="yes"/>
|
|
||||||
<usedfiles visible="$SHOW_USED_FILES"/>
|
|
||||||
<authorsection visible="yes"/>
|
|
||||||
</class>
|
|
||||||
|
|
||||||
<!-- Layout definition for a namespace page -->
|
|
||||||
<namespace>
|
|
||||||
<briefdescription visible="yes"/>
|
|
||||||
<memberdecl>
|
|
||||||
<nestednamespaces visible="yes" title=""/>
|
|
||||||
<constantgroups visible="yes" title=""/>
|
|
||||||
<interfaces visible="yes" title=""/>
|
|
||||||
<classes visible="yes" title=""/>
|
|
||||||
<concepts visible="yes" title=""/>
|
|
||||||
<structs visible="yes" title=""/>
|
|
||||||
<exceptions visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<sequences visible="yes" title=""/>
|
|
||||||
<dictionaries visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
<membergroups visible="yes" visible="yes"/>
|
|
||||||
</memberdecl>
|
|
||||||
<detaileddescription visible="yes" title=""/>
|
|
||||||
<memberdef>
|
|
||||||
<inlineclasses visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<sequences visible="yes" title=""/>
|
|
||||||
<dictionaries visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
</memberdef>
|
|
||||||
<authorsection visible="yes"/>
|
|
||||||
</namespace>
|
|
||||||
|
|
||||||
<!-- Layout definition for a concept page -->
|
|
||||||
<concept>
|
|
||||||
<briefdescription visible="yes"/>
|
|
||||||
<includes visible="$SHOW_HEADERFILE"/>
|
|
||||||
<definition visible="yes" title=""/>
|
|
||||||
<detaileddescription visible="yes" title=""/>
|
|
||||||
<authorsection visible="yes"/>
|
|
||||||
</concept>
|
|
||||||
|
|
||||||
<!-- Layout definition for a file page -->
|
|
||||||
<file>
|
|
||||||
<briefdescription visible="yes"/>
|
|
||||||
<includes visible="$SHOW_INCLUDE_FILES"/>
|
|
||||||
<includegraph visible="yes"/>
|
|
||||||
<includedbygraph visible="yes"/>
|
|
||||||
<sourcelink visible="yes"/>
|
|
||||||
<memberdecl>
|
|
||||||
<interfaces visible="yes" title=""/>
|
|
||||||
<classes visible="yes" title=""/>
|
|
||||||
<structs visible="yes" title=""/>
|
|
||||||
<exceptions visible="yes" title=""/>
|
|
||||||
<namespaces visible="yes" title=""/>
|
|
||||||
<concepts visible="yes" title=""/>
|
|
||||||
<constantgroups visible="yes" title=""/>
|
|
||||||
<defines visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<sequences visible="yes" title=""/>
|
|
||||||
<dictionaries visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
<membergroups visible="yes" visible="yes"/>
|
|
||||||
</memberdecl>
|
|
||||||
<detaileddescription visible="yes" title=""/>
|
|
||||||
<memberdef>
|
|
||||||
<inlineclasses visible="yes" title=""/>
|
|
||||||
<defines visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<sequences visible="yes" title=""/>
|
|
||||||
<dictionaries visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
</memberdef>
|
|
||||||
<authorsection/>
|
|
||||||
</file>
|
|
||||||
|
|
||||||
<!-- Layout definition for a group page -->
|
|
||||||
<group>
|
|
||||||
<briefdescription visible="yes"/>
|
|
||||||
<groupgraph visible="yes"/>
|
|
||||||
<memberdecl>
|
|
||||||
<nestedgroups visible="yes" title=""/>
|
|
||||||
<modules visible="yes" title=""/>
|
|
||||||
<dirs visible="yes" title=""/>
|
|
||||||
<files visible="yes" title=""/>
|
|
||||||
<namespaces visible="yes" title=""/>
|
|
||||||
<concepts visible="yes" title=""/>
|
|
||||||
<classes visible="yes" title=""/>
|
|
||||||
<defines visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<sequences visible="yes" title=""/>
|
|
||||||
<dictionaries visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<enumvalues visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<signals visible="yes" title=""/>
|
|
||||||
<publicslots visible="yes" title=""/>
|
|
||||||
<protectedslots visible="yes" title=""/>
|
|
||||||
<privateslots visible="yes" title=""/>
|
|
||||||
<events visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
<friends visible="yes" title=""/>
|
|
||||||
<membergroups visible="yes"/>
|
|
||||||
</memberdecl>
|
|
||||||
<detaileddescription visible="yes" title=""/>
|
|
||||||
<memberdef>
|
|
||||||
<pagedocs/>
|
|
||||||
<inlineclasses visible="yes" title=""/>
|
|
||||||
<defines visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<sequences visible="yes" title=""/>
|
|
||||||
<dictionaries visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<enumvalues visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<signals visible="yes" title=""/>
|
|
||||||
<publicslots visible="yes" title=""/>
|
|
||||||
<protectedslots visible="yes" title=""/>
|
|
||||||
<privateslots visible="yes" title=""/>
|
|
||||||
<events visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
<friends visible="yes" title=""/>
|
|
||||||
</memberdef>
|
|
||||||
<authorsection visible="yes"/>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<!-- Layout definition for a C++20 module page -->
|
|
||||||
<module>
|
|
||||||
<briefdescription visible="yes"/>
|
|
||||||
<exportedmodules visible="yes"/>
|
|
||||||
<memberdecl>
|
|
||||||
<concepts visible="yes" title=""/>
|
|
||||||
<classes visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<membergroups visible="yes" title=""/>
|
|
||||||
</memberdecl>
|
|
||||||
<detaileddescription visible="yes" title=""/>
|
|
||||||
<memberdecl>
|
|
||||||
<files visible="yes"/>
|
|
||||||
</memberdecl>
|
|
||||||
</module>
|
|
||||||
|
|
||||||
<!-- Layout definition for a directory page -->
|
|
||||||
<directory>
|
|
||||||
<briefdescription visible="yes"/>
|
|
||||||
<directorygraph visible="yes"/>
|
|
||||||
<memberdecl>
|
|
||||||
<dirs visible="yes"/>
|
|
||||||
<files visible="yes"/>
|
|
||||||
</memberdecl>
|
|
||||||
<detaileddescription visible="yes" title=""/>
|
|
||||||
</directory>
|
|
||||||
</doxygenlayout>
|
</doxygenlayout>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,7 @@ pub enum Meaning {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Lot {
|
impl Lot {
|
||||||
pub fn get_text1(&self) -> Option<Cow<'_, str>> {
|
pub fn get_text1(&self) -> Option<Cow<str>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Summary(summary) => match &summary.prefix {
|
Self::Summary(summary) => match &summary.prefix {
|
||||||
None => None,
|
None => None,
|
||||||
@@ -45,30 +45,28 @@ impl Lot {
|
|||||||
Self::Qr(qr) => match qr {
|
Self::Qr(qr) => match qr {
|
||||||
Qr::AskVerifyContact { .. } => None,
|
Qr::AskVerifyContact { .. } => None,
|
||||||
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||||
Qr::AskJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
|
||||||
Qr::FprOk { .. } => None,
|
Qr::FprOk { .. } => None,
|
||||||
Qr::FprMismatch { .. } => None,
|
Qr::FprMismatch { .. } => None,
|
||||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
|
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,
|
||||||
|
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
|
||||||
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
|
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
|
||||||
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
|
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
|
||||||
Qr::Url { url } => Some(Cow::Borrowed(url)),
|
Qr::Url { url } => Some(Cow::Borrowed(url)),
|
||||||
Qr::Text { text } => Some(Cow::Borrowed(text)),
|
Qr::Text { text } => Some(Cow::Borrowed(text)),
|
||||||
Qr::WithdrawVerifyContact { .. } => None,
|
Qr::WithdrawVerifyContact { .. } => None,
|
||||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
Qr::WithdrawVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||||
Qr::WithdrawJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
|
||||||
Qr::ReviveVerifyContact { .. } => None,
|
Qr::ReviveVerifyContact { .. } => None,
|
||||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
Qr::ReviveVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||||
Qr::ReviveJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
|
||||||
Qr::Login { address, .. } => Some(Cow::Borrowed(address)),
|
Qr::Login { address, .. } => Some(Cow::Borrowed(address)),
|
||||||
},
|
},
|
||||||
Self::Error(err) => Some(Cow::Borrowed(err)),
|
Self::Error(err) => Some(Cow::Borrowed(err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_text2(&self) -> Option<Cow<'_, str>> {
|
pub fn get_text2(&self) -> Option<Cow<str>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Summary(summary) => Some(summary.truncated_text(160)),
|
Self::Summary(summary) => Some(summary.truncated_text(160)),
|
||||||
Self::Qr(_) => None,
|
Self::Qr(_) => None,
|
||||||
@@ -101,23 +99,21 @@ impl Lot {
|
|||||||
Self::Qr(qr) => match qr {
|
Self::Qr(qr) => match qr {
|
||||||
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
|
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
|
||||||
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
||||||
Qr::AskJoinBroadcast { .. } => LotState::QrAskJoinBroadcast,
|
|
||||||
Qr::FprOk { .. } => LotState::QrFprOk,
|
Qr::FprOk { .. } => LotState::QrFprOk,
|
||||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
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,
|
||||||
|
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||||
Qr::Proxy { .. } => LotState::QrProxy,
|
Qr::Proxy { .. } => LotState::QrProxy,
|
||||||
Qr::Addr { .. } => LotState::QrAddr,
|
Qr::Addr { .. } => LotState::QrAddr,
|
||||||
Qr::Url { .. } => LotState::QrUrl,
|
Qr::Url { .. } => LotState::QrUrl,
|
||||||
Qr::Text { .. } => LotState::QrText,
|
Qr::Text { .. } => LotState::QrText,
|
||||||
Qr::WithdrawVerifyContact { .. } => LotState::QrWithdrawVerifyContact,
|
Qr::WithdrawVerifyContact { .. } => LotState::QrWithdrawVerifyContact,
|
||||||
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
||||||
Qr::WithdrawJoinBroadcast { .. } => LotState::QrWithdrawJoinBroadcast,
|
|
||||||
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
||||||
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
||||||
Qr::ReviveJoinBroadcast { .. } => LotState::QrReviveJoinBroadcast,
|
|
||||||
Qr::Login { .. } => LotState::QrLogin,
|
Qr::Login { .. } => LotState::QrLogin,
|
||||||
},
|
},
|
||||||
Self::Error(_err) => LotState::QrError,
|
Self::Error(_err) => LotState::QrError,
|
||||||
@@ -130,23 +126,21 @@ impl Lot {
|
|||||||
Self::Qr(qr) => match qr {
|
Self::Qr(qr) => match qr {
|
||||||
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||||
Qr::AskVerifyGroup { .. } => Default::default(),
|
Qr::AskVerifyGroup { .. } => 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::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
||||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
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(),
|
||||||
|
Qr::WebrtcInstance { .. } => Default::default(),
|
||||||
Qr::Proxy { .. } => Default::default(),
|
Qr::Proxy { .. } => Default::default(),
|
||||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||||
Qr::Url { .. } => Default::default(),
|
Qr::Url { .. } => Default::default(),
|
||||||
Qr::Text { .. } => Default::default(),
|
Qr::Text { .. } => Default::default(),
|
||||||
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||||
Qr::WithdrawVerifyGroup { .. } | Qr::WithdrawJoinBroadcast { .. } => {
|
Qr::WithdrawVerifyGroup { .. } => Default::default(),
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||||
Qr::ReviveVerifyGroup { .. } | Qr::ReviveJoinBroadcast { .. } => Default::default(),
|
Qr::ReviveVerifyGroup { .. } => Default::default(),
|
||||||
Qr::Login { .. } => Default::default(),
|
Qr::Login { .. } => Default::default(),
|
||||||
},
|
},
|
||||||
Self::Error(_) => Default::default(),
|
Self::Error(_) => Default::default(),
|
||||||
@@ -175,9 +169,6 @@ pub enum LotState {
|
|||||||
/// text1=groupname
|
/// text1=groupname
|
||||||
QrAskVerifyGroup = 202,
|
QrAskVerifyGroup = 202,
|
||||||
|
|
||||||
/// text1=broadcast_name
|
|
||||||
QrAskJoinBroadcast = 204,
|
|
||||||
|
|
||||||
/// id=contact
|
/// id=contact
|
||||||
QrFprOk = 210,
|
QrFprOk = 210,
|
||||||
|
|
||||||
@@ -194,6 +185,9 @@ pub enum LotState {
|
|||||||
|
|
||||||
QrBackupTooNew = 255,
|
QrBackupTooNew = 255,
|
||||||
|
|
||||||
|
/// text1=domain, text2=instance pattern
|
||||||
|
QrWebrtcInstance = 260,
|
||||||
|
|
||||||
/// text1=address, text2=protocol
|
/// text1=address, text2=protocol
|
||||||
QrProxy = 271,
|
QrProxy = 271,
|
||||||
|
|
||||||
@@ -213,15 +207,11 @@ pub enum LotState {
|
|||||||
|
|
||||||
/// text1=groupname
|
/// text1=groupname
|
||||||
QrWithdrawVerifyGroup = 502,
|
QrWithdrawVerifyGroup = 502,
|
||||||
/// text1=broadcast channel name
|
|
||||||
QrWithdrawJoinBroadcast = 504,
|
|
||||||
|
|
||||||
QrReviveVerifyContact = 510,
|
QrReviveVerifyContact = 510,
|
||||||
|
|
||||||
/// text1=groupname
|
/// text1=groupname
|
||||||
QrReviveVerifyGroup = 512,
|
QrReviveVerifyGroup = 512,
|
||||||
/// text1=groupname
|
|
||||||
QrReviveJoinBroadcast = 514,
|
|
||||||
|
|
||||||
/// text1=email_address
|
/// text1=email_address
|
||||||
QrLogin = 520,
|
QrLogin = 520,
|
||||||
@@ -230,6 +220,7 @@ pub enum LotState {
|
|||||||
MsgInFresh = 10,
|
MsgInFresh = 10,
|
||||||
MsgInNoticed = 13,
|
MsgInNoticed = 13,
|
||||||
MsgInSeen = 16,
|
MsgInSeen = 16,
|
||||||
|
MsgOutPreparing = 18,
|
||||||
MsgOutDraft = 19,
|
MsgOutDraft = 19,
|
||||||
MsgOutPending = 20,
|
MsgOutPending = 20,
|
||||||
MsgOutFailed = 24,
|
MsgOutFailed = 24,
|
||||||
@@ -245,6 +236,7 @@ impl From<MessageState> for LotState {
|
|||||||
InFresh => LotState::MsgInFresh,
|
InFresh => LotState::MsgInFresh,
|
||||||
InNoticed => LotState::MsgInNoticed,
|
InNoticed => LotState::MsgInNoticed,
|
||||||
InSeen => LotState::MsgInSeen,
|
InSeen => LotState::MsgInSeen,
|
||||||
|
OutPreparing => LotState::MsgOutPreparing,
|
||||||
OutDraft => LotState::MsgOutDraft,
|
OutDraft => LotState::MsgOutDraft,
|
||||||
OutPending => LotState::MsgOutPending,
|
OutPending => LotState::MsgOutPending,
|
||||||
OutFailed => LotState::MsgOutFailed,
|
OutFailed => LotState::MsgOutFailed,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-jsonrpc"
|
name = "deltachat-jsonrpc"
|
||||||
version = "2.50.0-dev"
|
version = "1.159.3"
|
||||||
description = "DeltaChat JSON-RPC API"
|
description = "DeltaChat JSON-RPC API"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
@@ -19,6 +19,7 @@ yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
|
|||||||
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
|
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
sanitize-filename = { workspace = true }
|
sanitize-filename = { workspace = true }
|
||||||
|
walkdir = "2.5.0"
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ pub enum Account {
|
|||||||
display_name: Option<String>,
|
display_name: Option<String>,
|
||||||
addr: Option<String>,
|
addr: Option<String>,
|
||||||
// size: u32,
|
// size: u32,
|
||||||
profile_image: Option<String>,
|
profile_image: Option<String>, // TODO: This needs to be converted to work with blob http server.
|
||||||
color: String,
|
color: String,
|
||||||
/// Optional tag as "Work", "Family".
|
/// Optional tag as "Work", "Family".
|
||||||
/// Meant to help profile owner to differ between profiles with similar names.
|
/// Meant to help profile owner to differ between profiles with similar names.
|
||||||
@@ -32,10 +32,7 @@ impl Account {
|
|||||||
let addr = ctx.get_config(Config::Addr).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).await?.get_color(),
|
||||||
.await?
|
|
||||||
.get_or_gen_color(ctx)
|
|
||||||
.await?,
|
|
||||||
);
|
);
|
||||||
let private_tag = ctx.get_config(Config::PrivateTag).await?;
|
let private_tag = ctx.get_config(Config::PrivateTag).await?;
|
||||||
Ok(Account::Configured {
|
Ok(Account::Configured {
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
use anyhow::{Context as _, Result};
|
|
||||||
|
|
||||||
use deltachat::calls::{call_state, CallState};
|
|
||||||
use deltachat::context::Context;
|
|
||||||
use deltachat::message::MsgId;
|
|
||||||
use serde::Serialize;
|
|
||||||
use typescript_type_def::TypeDef;
|
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
|
||||||
#[serde(rename = "CallInfo", rename_all = "camelCase")]
|
|
||||||
pub struct JsonrpcCallInfo {
|
|
||||||
/// SDP offer.
|
|
||||||
///
|
|
||||||
/// Can be used to manually answer the call
|
|
||||||
/// even if incoming call event was missed.
|
|
||||||
pub sdp_offer: String,
|
|
||||||
|
|
||||||
/// True if the call is started as a video call.
|
|
||||||
pub has_video: bool,
|
|
||||||
|
|
||||||
/// Call state.
|
|
||||||
///
|
|
||||||
/// For example, if the call is accepted, active, canceled, declined etc.
|
|
||||||
pub state: JsonrpcCallState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JsonrpcCallInfo {
|
|
||||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<JsonrpcCallInfo> {
|
|
||||||
let call_info = context.load_call_by_id(msg_id).await?.with_context(|| {
|
|
||||||
format!("Attempting to get call state of non-call message {msg_id}")
|
|
||||||
})?;
|
|
||||||
let sdp_offer = call_info.place_call_info.clone();
|
|
||||||
let has_video = call_info.has_video_initially();
|
|
||||||
let state = JsonrpcCallState::from_msg_id(context, msg_id).await?;
|
|
||||||
|
|
||||||
Ok(JsonrpcCallInfo {
|
|
||||||
sdp_offer,
|
|
||||||
has_video,
|
|
||||||
state,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
|
||||||
#[serde(rename = "CallState", tag = "kind")]
|
|
||||||
pub enum JsonrpcCallState {
|
|
||||||
/// Fresh incoming or outgoing call that is still ringing.
|
|
||||||
///
|
|
||||||
/// There is no separate state for outgoing call
|
|
||||||
/// that has been dialled but not ringing on the other side yet
|
|
||||||
/// as we don't know whether the other side received our call.
|
|
||||||
Alerting,
|
|
||||||
|
|
||||||
/// Active call.
|
|
||||||
Active,
|
|
||||||
|
|
||||||
/// Completed call that was once active
|
|
||||||
/// and then was terminated for any reason.
|
|
||||||
Completed {
|
|
||||||
/// Call duration in seconds.
|
|
||||||
duration: i64,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Incoming call that was not picked up within a timeout
|
|
||||||
/// or was explicitly ended by the caller before we picked up.
|
|
||||||
Missed,
|
|
||||||
|
|
||||||
/// Incoming call that was explicitly ended on our side
|
|
||||||
/// before picking up or outgoing call
|
|
||||||
/// that was declined before the timeout.
|
|
||||||
Declined,
|
|
||||||
|
|
||||||
/// Outgoing call that has been canceled on our side
|
|
||||||
/// before receiving a response.
|
|
||||||
///
|
|
||||||
/// Incoming calls cannot be canceled,
|
|
||||||
/// on the receiver side canceled calls
|
|
||||||
/// usually result in missed calls.
|
|
||||||
Canceled,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JsonrpcCallState {
|
|
||||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<JsonrpcCallState> {
|
|
||||||
let call_state = call_state(context, msg_id).await?;
|
|
||||||
|
|
||||||
let jsonrpc_call_state = match call_state {
|
|
||||||
CallState::Alerting => JsonrpcCallState::Alerting,
|
|
||||||
CallState::Active => JsonrpcCallState::Active,
|
|
||||||
CallState::Completed { duration } => JsonrpcCallState::Completed { duration },
|
|
||||||
CallState::Missed => JsonrpcCallState::Missed,
|
|
||||||
CallState::Declined => JsonrpcCallState::Declined,
|
|
||||||
CallState::Canceled => JsonrpcCallState::Canceled,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(jsonrpc_call_state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,10 +6,12 @@ use deltachat::chat::{Chat, ChatId};
|
|||||||
use deltachat::constants::Chattype;
|
use deltachat::constants::Chattype;
|
||||||
use deltachat::contact::{Contact, ContactId};
|
use deltachat::contact::{Contact, ContactId};
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
|
use num_traits::cast::ToPrimitive;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
|
use super::contact::ContactObject;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -17,36 +19,25 @@ pub struct FullChat {
|
|||||||
id: u32,
|
id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
|
|
||||||
/// True if the chat is encrypted.
|
/// True if the chat is protected.
|
||||||
/// This means that all messages in the chat are encrypted,
|
|
||||||
/// and all contacts in the chat are "key-contacts",
|
|
||||||
/// i.e. identified by the PGP key fingerprint.
|
|
||||||
///
|
///
|
||||||
/// False if the chat is unencrypted.
|
/// UI should display a green checkmark
|
||||||
/// This means that all messages in the chat are unencrypted,
|
/// in the chat title,
|
||||||
/// and all contacts in the chat are "address-contacts",
|
/// in the chat profile title and
|
||||||
/// i.e. identified by the email address.
|
/// in the chatlist item
|
||||||
/// The UI should mark this chat e.g. with a mail-letter icon.
|
/// if chat protection is enabled.
|
||||||
///
|
/// UI should also display a green checkmark
|
||||||
/// Unencrypted groups are called "ad-hoc groups"
|
/// in the contact profile
|
||||||
/// and the user can't add/remove members,
|
/// if 1:1 chat with this contact exists and is protected.
|
||||||
/// create a QR invite code,
|
is_protected: bool,
|
||||||
/// or set an avatar.
|
|
||||||
/// These options should therefore be disabled in the UI.
|
|
||||||
///
|
|
||||||
/// Note that it can happen that an encrypted chat
|
|
||||||
/// contains unencrypted messages that were received in core <= v1.159.*
|
|
||||||
/// and vice versa.
|
|
||||||
///
|
|
||||||
/// See also `is_key_contact` on `Contact`.
|
|
||||||
is_encrypted: bool,
|
|
||||||
profile_image: Option<String>, //BLOBS ?
|
profile_image: Option<String>, //BLOBS ?
|
||||||
archived: bool,
|
archived: bool,
|
||||||
pinned: bool,
|
pinned: bool,
|
||||||
// subtitle - will be moved to frontend because it uses translation functions
|
// subtitle - will be moved to frontend because it uses translation functions
|
||||||
chat_type: JsonrpcChatType,
|
chat_type: u32,
|
||||||
is_unpromoted: bool,
|
is_unpromoted: bool,
|
||||||
is_self_talk: bool,
|
is_self_talk: bool,
|
||||||
|
contacts: Vec<ContactObject>,
|
||||||
contact_ids: Vec<u32>,
|
contact_ids: Vec<u32>,
|
||||||
|
|
||||||
/// Contact IDs of the past chat members.
|
/// Contact IDs of the past chat members.
|
||||||
@@ -56,18 +47,11 @@ pub struct FullChat {
|
|||||||
fresh_message_counter: usize,
|
fresh_message_counter: usize,
|
||||||
// is_group - please check over chat.type in frontend instead
|
// is_group - please check over chat.type in frontend instead
|
||||||
is_contact_request: bool,
|
is_contact_request: bool,
|
||||||
|
is_protection_broken: bool,
|
||||||
is_device_chat: bool,
|
is_device_chat: bool,
|
||||||
/// Note that this is different from
|
|
||||||
/// [`ChatListItem::is_self_in_group`](`crate::api::types::chat_list::ChatListItemFetchResult::ChatListItem::is_self_in_group`).
|
|
||||||
/// This property should only be accessed
|
|
||||||
/// when [`FullChat::chat_type`] is [`Chattype::Group`].
|
|
||||||
//
|
|
||||||
// We could utilize [`Chat::is_self_in_chat`],
|
|
||||||
// but that would be an extra DB query.
|
|
||||||
self_in_group: bool,
|
self_in_group: bool,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
ephemeral_timer: u32,
|
ephemeral_timer: u32, //TODO look if there are more important properties in newer core versions
|
||||||
can_send: bool,
|
can_send: bool,
|
||||||
was_seen_recently: bool,
|
was_seen_recently: bool,
|
||||||
mailing_list_address: Option<String>,
|
mailing_list_address: Option<String>,
|
||||||
@@ -81,6 +65,20 @@ impl FullChat {
|
|||||||
let contact_ids = get_chat_contacts(context, rust_chat_id).await?;
|
let contact_ids = get_chat_contacts(context, rust_chat_id).await?;
|
||||||
let past_contact_ids = get_past_chat_contacts(context, rust_chat_id).await?;
|
let past_contact_ids = get_past_chat_contacts(context, rust_chat_id).await?;
|
||||||
|
|
||||||
|
let mut contacts = Vec::with_capacity(contact_ids.len());
|
||||||
|
|
||||||
|
for contact_id in &contact_ids {
|
||||||
|
contacts.push(
|
||||||
|
ContactObject::try_from_dc_contact(
|
||||||
|
context,
|
||||||
|
Contact::get_by_id(context, *contact_id)
|
||||||
|
.await
|
||||||
|
.context("failed to load contact")?,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let profile_image = match chat.get_profile_image(context).await? {
|
let profile_image = match chat.get_profile_image(context).await? {
|
||||||
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,
|
||||||
@@ -109,18 +107,20 @@ impl FullChat {
|
|||||||
Ok(FullChat {
|
Ok(FullChat {
|
||||||
id: chat_id,
|
id: chat_id,
|
||||||
name: chat.name.clone(),
|
name: chat.name.clone(),
|
||||||
is_encrypted: chat.is_encrypted(context).await?,
|
is_protected: chat.is_protected(),
|
||||||
profile_image, //BLOBS ?
|
profile_image, //BLOBS ?
|
||||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||||
chat_type: chat.get_type().into(),
|
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||||
is_unpromoted: chat.is_unpromoted(),
|
is_unpromoted: chat.is_unpromoted(),
|
||||||
is_self_talk: chat.is_self_talk(),
|
is_self_talk: chat.is_self_talk(),
|
||||||
|
contacts,
|
||||||
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
|
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||||
past_contact_ids: past_contact_ids.iter().map(|id| id.to_u32()).collect(),
|
past_contact_ids: past_contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||||
color,
|
color,
|
||||||
fresh_message_counter,
|
fresh_message_counter,
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
|
is_protection_broken: chat.is_protection_broken(),
|
||||||
is_device_chat: chat.is_device_talk(),
|
is_device_chat: chat.is_device_talk(),
|
||||||
self_in_group: contact_ids.contains(&ContactId::SELF),
|
self_in_group: contact_ids.contains(&ContactId::SELF),
|
||||||
is_muted: chat.is_muted(),
|
is_muted: chat.is_muted(),
|
||||||
@@ -133,6 +133,7 @@ impl FullChat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// cheaper version of fullchat, omits:
|
/// cheaper version of fullchat, omits:
|
||||||
|
/// - contacts
|
||||||
/// - contact_ids
|
/// - contact_ids
|
||||||
/// - fresh_message_counter
|
/// - fresh_message_counter
|
||||||
/// - ephemeral_timer
|
/// - ephemeral_timer
|
||||||
@@ -147,38 +148,26 @@ pub struct BasicChat {
|
|||||||
id: u32,
|
id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
|
|
||||||
/// True if the chat is encrypted.
|
/// True if the chat is protected.
|
||||||
/// This means that all messages in the chat are encrypted,
|
|
||||||
/// and all contacts in the chat are "key-contacts",
|
|
||||||
/// i.e. identified by the PGP key fingerprint.
|
|
||||||
///
|
///
|
||||||
/// False if the chat is unencrypted.
|
/// UI should display a green checkmark
|
||||||
/// This means that all messages in the chat are unencrypted,
|
/// in the chat title,
|
||||||
/// and all contacts in the chat are "address-contacts",
|
/// in the chat profile title and
|
||||||
/// i.e. identified by the email address.
|
/// in the chatlist item
|
||||||
/// The UI should mark this chat e.g. with a mail-letter icon.
|
/// if chat protection is enabled.
|
||||||
///
|
/// UI should also display a green checkmark
|
||||||
/// Unencrypted groups are called "ad-hoc groups"
|
/// in the contact profile
|
||||||
/// and the user can't add/remove members,
|
/// if 1:1 chat with this contact exists and is protected.
|
||||||
/// create a QR invite code,
|
is_protected: bool,
|
||||||
/// or set an avatar.
|
|
||||||
/// These options should therefore be disabled in the UI.
|
|
||||||
///
|
|
||||||
/// Note that it can happen that an encrypted chat
|
|
||||||
/// contains unencrypted messages that were received in core <= v1.159.*
|
|
||||||
/// and vice versa.
|
|
||||||
///
|
|
||||||
/// See also `is_key_contact` on `Contact`.
|
|
||||||
is_encrypted: bool,
|
|
||||||
profile_image: Option<String>, //BLOBS ?
|
profile_image: Option<String>, //BLOBS ?
|
||||||
archived: bool,
|
archived: bool,
|
||||||
pinned: bool,
|
pinned: bool,
|
||||||
chat_type: JsonrpcChatType,
|
chat_type: u32,
|
||||||
is_unpromoted: bool,
|
is_unpromoted: bool,
|
||||||
is_self_talk: bool,
|
is_self_talk: bool,
|
||||||
color: String,
|
color: String,
|
||||||
is_contact_request: bool,
|
is_contact_request: bool,
|
||||||
|
is_protection_broken: bool,
|
||||||
is_device_chat: bool,
|
is_device_chat: bool,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
}
|
}
|
||||||
@@ -197,15 +186,16 @@ impl BasicChat {
|
|||||||
Ok(BasicChat {
|
Ok(BasicChat {
|
||||||
id: chat_id,
|
id: chat_id,
|
||||||
name: chat.name.clone(),
|
name: chat.name.clone(),
|
||||||
is_encrypted: chat.is_encrypted(context).await?,
|
is_protected: chat.is_protected(),
|
||||||
profile_image, //BLOBS ?
|
profile_image, //BLOBS ?
|
||||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||||
chat_type: chat.get_type().into(),
|
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||||
is_unpromoted: chat.is_unpromoted(),
|
is_unpromoted: chat.is_unpromoted(),
|
||||||
is_self_talk: chat.is_self_talk(),
|
is_self_talk: chat.is_self_talk(),
|
||||||
color,
|
color,
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
|
is_protection_broken: chat.is_protection_broken(),
|
||||||
is_device_chat: chat.is_device_talk(),
|
is_device_chat: chat.is_device_talk(),
|
||||||
is_muted: chat.is_muted(),
|
is_muted: chat.is_muted(),
|
||||||
})
|
})
|
||||||
@@ -240,52 +230,18 @@ impl MuteDuration {
|
|||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "ChatVisibility")]
|
#[serde(rename = "ChatVisibility")]
|
||||||
pub enum JsonrpcChatVisibility {
|
pub enum JSONRPCChatVisibility {
|
||||||
Normal,
|
Normal,
|
||||||
Archived,
|
Archived,
|
||||||
Pinned,
|
Pinned,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JsonrpcChatVisibility {
|
impl JSONRPCChatVisibility {
|
||||||
pub fn into_core_type(self) -> ChatVisibility {
|
pub fn into_core_type(self) -> ChatVisibility {
|
||||||
match self {
|
match self {
|
||||||
JsonrpcChatVisibility::Normal => ChatVisibility::Normal,
|
JSONRPCChatVisibility::Normal => ChatVisibility::Normal,
|
||||||
JsonrpcChatVisibility::Archived => ChatVisibility::Archived,
|
JSONRPCChatVisibility::Archived => ChatVisibility::Archived,
|
||||||
JsonrpcChatVisibility::Pinned => ChatVisibility::Pinned,
|
JSONRPCChatVisibility::Pinned => ChatVisibility::Pinned,
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, PartialEq, TypeDef, schemars::JsonSchema)]
|
|
||||||
#[serde(rename = "ChatType")]
|
|
||||||
pub enum JsonrpcChatType {
|
|
||||||
Single,
|
|
||||||
Group,
|
|
||||||
Mailinglist,
|
|
||||||
OutBroadcast,
|
|
||||||
InBroadcast,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Chattype> for JsonrpcChatType {
|
|
||||||
fn from(chattype: Chattype) -> Self {
|
|
||||||
match chattype {
|
|
||||||
Chattype::Single => JsonrpcChatType::Single,
|
|
||||||
Chattype::Group => JsonrpcChatType::Group,
|
|
||||||
Chattype::Mailinglist => JsonrpcChatType::Mailinglist,
|
|
||||||
Chattype::OutBroadcast => JsonrpcChatType::OutBroadcast,
|
|
||||||
Chattype::InBroadcast => JsonrpcChatType::InBroadcast,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<JsonrpcChatType> for Chattype {
|
|
||||||
fn from(chattype: JsonrpcChatType) -> Self {
|
|
||||||
match chattype {
|
|
||||||
JsonrpcChatType::Single => Chattype::Single,
|
|
||||||
JsonrpcChatType::Group => Chattype::Group,
|
|
||||||
JsonrpcChatType::Mailinglist => Chattype::Mailinglist,
|
|
||||||
JsonrpcChatType::OutBroadcast => Chattype::OutBroadcast,
|
|
||||||
JsonrpcChatType::InBroadcast => Chattype::InBroadcast,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use anyhow::{Context, Result};
|
|||||||
use deltachat::chat::{Chat, ChatId};
|
use deltachat::chat::{Chat, ChatId};
|
||||||
use deltachat::chatlist::get_last_message_for_chat;
|
use deltachat::chatlist::get_last_message_for_chat;
|
||||||
use deltachat::constants::*;
|
use deltachat::constants::*;
|
||||||
use deltachat::contact::Contact;
|
use deltachat::contact::{Contact, ContactId};
|
||||||
use deltachat::{
|
use deltachat::{
|
||||||
chat::{get_chat_contacts, ChatVisibility},
|
chat::{get_chat_contacts, ChatVisibility},
|
||||||
chatlist::Chatlist,
|
chatlist::Chatlist,
|
||||||
@@ -11,7 +11,6 @@ use num_traits::cast::ToPrimitive;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@@ -24,38 +23,13 @@ pub enum ChatListItemFetchResult {
|
|||||||
name: String,
|
name: String,
|
||||||
avatar_path: Option<String>,
|
avatar_path: Option<String>,
|
||||||
color: String,
|
color: String,
|
||||||
chat_type: JsonrpcChatType,
|
|
||||||
last_updated: Option<i64>,
|
last_updated: Option<i64>,
|
||||||
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
|
/// showing preview if last chat message is image
|
||||||
summary_preview_image: Option<String>,
|
summary_preview_image: Option<String>,
|
||||||
|
is_protected: bool,
|
||||||
/// True if the chat is encrypted.
|
|
||||||
/// This means that all messages in the chat are encrypted,
|
|
||||||
/// and all contacts in the chat are "key-contacts",
|
|
||||||
/// i.e. identified by the PGP key fingerprint.
|
|
||||||
///
|
|
||||||
/// False if the chat is unencrypted.
|
|
||||||
/// This means that all messages in the chat are unencrypted,
|
|
||||||
/// and all contacts in the chat are "address-contacts",
|
|
||||||
/// i.e. identified by the email address.
|
|
||||||
/// The UI should mark this chat e.g. with a mail-letter icon.
|
|
||||||
///
|
|
||||||
/// Unencrypted groups are called "ad-hoc groups"
|
|
||||||
/// and the user can't add/remove members,
|
|
||||||
/// create a QR invite code,
|
|
||||||
/// or set an avatar.
|
|
||||||
/// These options should therefore be disabled in the UI.
|
|
||||||
///
|
|
||||||
/// Note that it can happen that an encrypted chat
|
|
||||||
/// contains unencrypted messages that were received in core <= v1.159.*
|
|
||||||
/// and vice versa.
|
|
||||||
///
|
|
||||||
/// See also `is_key_contact` on `Contact`.
|
|
||||||
is_encrypted: bool,
|
|
||||||
/// deprecated 2025-07, use chat_type instead
|
|
||||||
is_group: bool,
|
is_group: bool,
|
||||||
fresh_message_counter: usize,
|
fresh_message_counter: usize,
|
||||||
is_self_talk: bool,
|
is_self_talk: bool,
|
||||||
@@ -66,6 +40,8 @@ pub enum ChatListItemFetchResult {
|
|||||||
is_pinned: bool,
|
is_pinned: bool,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
is_contact_request: bool,
|
is_contact_request: bool,
|
||||||
|
/// true when chat is a broadcastlist
|
||||||
|
is_broadcast: 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>,
|
||||||
was_seen_recently: bool,
|
was_seen_recently: bool,
|
||||||
@@ -127,8 +103,11 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
None => (None, None),
|
None => (None, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
||||||
|
|
||||||
|
let self_in_group = chat_contacts.contains(&ContactId::SELF);
|
||||||
|
|
||||||
let (dm_chat_contact, was_seen_recently) = 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 contact = chat_contacts.first();
|
let contact = chat_contacts.first();
|
||||||
let was_seen_recently = match contact {
|
let was_seen_recently = match contact {
|
||||||
Some(contact) => Contact::get_by_id(ctx, *contact)
|
Some(contact) => Contact::get_by_id(ctx, *contact)
|
||||||
@@ -152,23 +131,23 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
name: chat.get_name().to_owned(),
|
name: chat.get_name().to_owned(),
|
||||||
avatar_path,
|
avatar_path,
|
||||||
color,
|
color,
|
||||||
chat_type: chat.get_type().into(),
|
|
||||||
last_updated,
|
last_updated,
|
||||||
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,
|
summary_preview_image,
|
||||||
is_encrypted: chat.is_encrypted(ctx).await?,
|
is_protected: chat.is_protected(),
|
||||||
is_group: chat.get_type() == Chattype::Group,
|
is_group: chat.get_type() == Chattype::Group,
|
||||||
fresh_message_counter,
|
fresh_message_counter,
|
||||||
is_self_talk: chat.is_self_talk(),
|
is_self_talk: chat.is_self_talk(),
|
||||||
is_device_talk: chat.is_device_talk(),
|
is_device_talk: chat.is_device_talk(),
|
||||||
is_self_in_group: chat.is_self_in_chat(ctx).await?,
|
is_self_in_group: self_in_group,
|
||||||
is_sending_location: chat.is_sending_locations(),
|
is_sending_location: chat.is_sending_locations(),
|
||||||
is_archived: visibility == ChatVisibility::Archived,
|
is_archived: visibility == ChatVisibility::Archived,
|
||||||
is_pinned: visibility == ChatVisibility::Pinned,
|
is_pinned: visibility == ChatVisibility::Pinned,
|
||||||
is_muted: chat.is_muted(),
|
is_muted: chat.is_muted(),
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
|
is_broadcast: chat.get_type() == Chattype::Broadcast,
|
||||||
dm_chat_contact,
|
dm_chat_contact,
|
||||||
was_seen_recently,
|
was_seen_recently,
|
||||||
last_message_type: message_type,
|
last_message_type: message_type,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use deltachat::color;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::key::{DcKey, SignedPublicKey};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
@@ -19,47 +19,29 @@ pub struct ContactObject {
|
|||||||
profile_image: Option<String>, // BLOBS
|
profile_image: Option<String>, // BLOBS
|
||||||
name_and_addr: String,
|
name_and_addr: String,
|
||||||
is_blocked: bool,
|
is_blocked: bool,
|
||||||
|
|
||||||
/// Is the contact a key contact.
|
|
||||||
is_key_contact: bool,
|
|
||||||
|
|
||||||
/// Is encryption available for this contact.
|
|
||||||
///
|
|
||||||
/// This can only be true for key-contacts.
|
|
||||||
/// However, it is possible to have a key-contact
|
|
||||||
/// for which encryption is not available because we don't have a key yet,
|
|
||||||
/// e.g. if we just scanned the fingerprint from a QR code.
|
|
||||||
e2ee_avail: bool,
|
e2ee_avail: bool,
|
||||||
|
|
||||||
/// True if the contact
|
/// True if the contact can be added to verified groups.
|
||||||
/// 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.
|
/// If this is true
|
||||||
|
/// UI should display green checkmark after the contact name
|
||||||
|
/// in contact list items,
|
||||||
|
/// in chat member list items
|
||||||
|
/// and in profiles if no chat with the contact exist.
|
||||||
is_verified: bool,
|
is_verified: bool,
|
||||||
|
|
||||||
/// The contact ID that verified a contact.
|
/// True if the contact profile title should have a green checkmark.
|
||||||
///
|
///
|
||||||
/// As verifier may be unknown,
|
/// This indicates whether 1:1 chat has a green checkmark
|
||||||
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
|
/// or will have a green checkmark if created.
|
||||||
|
is_profile_verified: bool,
|
||||||
|
|
||||||
|
/// The ID of the contact that verified this contact.
|
||||||
///
|
///
|
||||||
/// UI should display the information in the contact's profile as follows:
|
/// If this is present,
|
||||||
///
|
/// display a green checkmark and "Introduced by ..."
|
||||||
/// - If `verifierId` != 0,
|
/// string followed by the verifier contact name and address
|
||||||
/// display text "Introduced by ..."
|
/// in the contact profile.
|
||||||
/// 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>,
|
verifier_id: Option<u32>,
|
||||||
|
|
||||||
/// the contact's last seen timestamp
|
/// the contact's last seen timestamp
|
||||||
@@ -80,11 +62,11 @@ impl ContactObject {
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
let is_verified = contact.is_verified(context).await?;
|
let is_verified = contact.is_verified(context).await?;
|
||||||
|
let is_profile_verified = contact.is_profile_verified(context).await?;
|
||||||
|
|
||||||
let verifier_id = contact
|
let verifier_id = contact
|
||||||
.get_verifier_id(context)
|
.get_verifier_id(context)
|
||||||
.await?
|
.await?
|
||||||
.flatten()
|
|
||||||
.map(|contact_id| contact_id.to_u32());
|
.map(|contact_id| contact_id.to_u32());
|
||||||
|
|
||||||
Ok(ContactObject {
|
Ok(ContactObject {
|
||||||
@@ -98,9 +80,9 @@ impl ContactObject {
|
|||||||
profile_image, //BLOBS
|
profile_image, //BLOBS
|
||||||
name_and_addr: contact.get_name_n_addr(),
|
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(),
|
|
||||||
e2ee_avail: contact.e2ee_avail(context).await?,
|
e2ee_avail: contact.e2ee_avail(context).await?,
|
||||||
is_verified,
|
is_verified,
|
||||||
|
is_profile_verified,
|
||||||
verifier_id,
|
verifier_id,
|
||||||
last_seen: contact.last_seen(),
|
last_seen: contact.last_seen(),
|
||||||
was_seen_recently: contact.was_seen_recently(),
|
was_seen_recently: contact.was_seen_recently(),
|
||||||
@@ -129,13 +111,7 @@ pub struct VcardContact {
|
|||||||
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
|
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
|
||||||
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
|
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
|
||||||
let display_name = vc.display_name().to_string();
|
let display_name = vc.display_name().to_string();
|
||||||
let is_self = false;
|
let color = color::str_to_color(&vc.addr.to_lowercase());
|
||||||
let fpr = vc.key.as_deref().and_then(|k| {
|
|
||||||
SignedPublicKey::from_base64(k)
|
|
||||||
.ok()
|
|
||||||
.map(|k| k.dc_fingerprint())
|
|
||||||
});
|
|
||||||
let color = deltachat::contact::get_color(is_self, &vc.addr, &fpr);
|
|
||||||
Self {
|
Self {
|
||||||
addr: vc.addr,
|
addr: vc.addr,
|
||||||
display_name,
|
display_name,
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ use deltachat::{Event as CoreEvent, EventType as CoreEventType};
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
use super::chat::JsonrpcChatType;
|
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Event {
|
pub struct Event {
|
||||||
@@ -226,6 +224,7 @@ pub enum EventType {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||||
|
/// Or the verify state of a chat has changed.
|
||||||
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
||||||
/// and removeContactFromChat().
|
/// and removeContactFromChat().
|
||||||
///
|
///
|
||||||
@@ -271,7 +270,7 @@ pub enum EventType {
|
|||||||
/// Progress.
|
/// Progress.
|
||||||
///
|
///
|
||||||
/// 0=error, 1-999=progress in permille, 1000=success and done
|
/// 0=error, 1-999=progress in permille, 1000=success and done
|
||||||
progress: u16,
|
progress: usize,
|
||||||
|
|
||||||
/// Progress comment or error, something to display to the user.
|
/// Progress comment or error, something to display to the user.
|
||||||
comment: Option<String>,
|
comment: Option<String>,
|
||||||
@@ -282,7 +281,7 @@ pub enum EventType {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ImexProgress {
|
ImexProgress {
|
||||||
/// 0=error, 1-999=progress in permille, 1000=success and done
|
/// 0=error, 1-999=progress in permille, 1000=success and done
|
||||||
progress: u16,
|
progress: usize,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// A file has been exported. A file has been written by imex().
|
/// A file has been exported. A file has been written by imex().
|
||||||
@@ -295,8 +294,8 @@ pub enum EventType {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ImexFileWritten { path: String },
|
ImexFileWritten { path: String },
|
||||||
|
|
||||||
/// Progress event sent when SecureJoin protocol has finished
|
/// Progress information of a secure-join handshake from the view of the inviter
|
||||||
/// from the view of the inviter (Alice, the person who shows the QR code).
|
/// (Alice, the person who shows the QR code).
|
||||||
///
|
///
|
||||||
/// These events are typically sent after a joiner has scanned the QR code
|
/// These events are typically sent after a joiner has scanned the QR code
|
||||||
/// generated by getChatSecurejoinQrCodeSvg().
|
/// generated by getChatSecurejoinQrCodeSvg().
|
||||||
@@ -305,15 +304,12 @@ pub enum EventType {
|
|||||||
/// ID of the contact that wants to join.
|
/// ID of the contact that wants to join.
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
|
|
||||||
/// The type of the joined chat.
|
/// Progress as:
|
||||||
/// This can take the same values
|
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||||
/// as `BasicChat.chatType` ([`crate::api::types::chat::BasicChat::chat_type`]).
|
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||||
chat_type: JsonrpcChatType,
|
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
|
||||||
/// ID of the chat in case of success.
|
/// 1000=Protocol finished for this contact.
|
||||||
chat_id: u32,
|
progress: usize,
|
||||||
|
|
||||||
/// Progress, always 1000.
|
|
||||||
progress: u16,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Progress information of a secure-join handshake from the view of the joiner
|
/// Progress information of a secure-join handshake from the view of the joiner
|
||||||
@@ -329,7 +325,7 @@ pub enum EventType {
|
|||||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, 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)
|
/// (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: usize,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// The connectivity to the server changed.
|
/// The connectivity to the server changed.
|
||||||
@@ -421,56 +417,6 @@ pub enum EventType {
|
|||||||
/// Number of events skipped.
|
/// Number of events skipped.
|
||||||
n: u64,
|
n: u64,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Incoming call.
|
|
||||||
IncomingCall {
|
|
||||||
/// ID of the info message referring to the call.
|
|
||||||
msg_id: u32,
|
|
||||||
/// ID of the chat which the message belongs to.
|
|
||||||
chat_id: u32,
|
|
||||||
/// User-defined info as passed to place_outgoing_call()
|
|
||||||
place_call_info: String,
|
|
||||||
/// True if incoming call is a video call.
|
|
||||||
has_video: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Incoming call accepted.
|
|
||||||
/// This is esp. interesting to stop ringing on other devices.
|
|
||||||
IncomingCallAccepted {
|
|
||||||
/// ID of the info message referring to the call.
|
|
||||||
msg_id: u32,
|
|
||||||
/// ID of the chat which the message belongs to.
|
|
||||||
chat_id: u32,
|
|
||||||
/// The call was accepted from this device (process).
|
|
||||||
from_this_device: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Outgoing call accepted.
|
|
||||||
OutgoingCallAccepted {
|
|
||||||
/// ID of the info message referring to the call.
|
|
||||||
msg_id: u32,
|
|
||||||
/// ID of the chat which the message belongs to.
|
|
||||||
chat_id: u32,
|
|
||||||
/// User-defined info passed to dc_accept_incoming_call(
|
|
||||||
accept_call_info: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Call ended.
|
|
||||||
CallEnded {
|
|
||||||
/// ID of the info message referring to the call.
|
|
||||||
msg_id: u32,
|
|
||||||
/// ID of the chat which the message belongs to.
|
|
||||||
chat_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// One or more transports has changed.
|
|
||||||
///
|
|
||||||
/// UI should update the list.
|
|
||||||
///
|
|
||||||
/// This event is emitted when transport
|
|
||||||
/// synchronization messages arrives,
|
|
||||||
/// but not when the UI modifies the transport list by itself.
|
|
||||||
TransportsModified,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CoreEventType> for EventType {
|
impl From<CoreEventType> for EventType {
|
||||||
@@ -577,13 +523,9 @@ impl From<CoreEventType> for EventType {
|
|||||||
},
|
},
|
||||||
CoreEventType::SecurejoinInviterProgress {
|
CoreEventType::SecurejoinInviterProgress {
|
||||||
contact_id,
|
contact_id,
|
||||||
chat_type,
|
|
||||||
chat_id,
|
|
||||||
progress,
|
progress,
|
||||||
} => SecurejoinInviterProgress {
|
} => SecurejoinInviterProgress {
|
||||||
contact_id: contact_id.to_u32(),
|
contact_id: contact_id.to_u32(),
|
||||||
chat_type: chat_type.into(),
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
progress,
|
progress,
|
||||||
},
|
},
|
||||||
CoreEventType::SecurejoinJoinerProgress {
|
CoreEventType::SecurejoinJoinerProgress {
|
||||||
@@ -625,41 +567,6 @@ impl From<CoreEventType> for EventType {
|
|||||||
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
||||||
CoreEventType::AccountsChanged => AccountsChanged,
|
CoreEventType::AccountsChanged => AccountsChanged,
|
||||||
CoreEventType::AccountsItemChanged => AccountsItemChanged,
|
CoreEventType::AccountsItemChanged => AccountsItemChanged,
|
||||||
CoreEventType::IncomingCall {
|
|
||||||
msg_id,
|
|
||||||
chat_id,
|
|
||||||
place_call_info,
|
|
||||||
has_video,
|
|
||||||
} => IncomingCall {
|
|
||||||
msg_id: msg_id.to_u32(),
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
place_call_info,
|
|
||||||
has_video,
|
|
||||||
},
|
|
||||||
CoreEventType::IncomingCallAccepted {
|
|
||||||
msg_id,
|
|
||||||
chat_id,
|
|
||||||
from_this_device,
|
|
||||||
} => IncomingCallAccepted {
|
|
||||||
msg_id: msg_id.to_u32(),
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
from_this_device,
|
|
||||||
},
|
|
||||||
CoreEventType::OutgoingCallAccepted {
|
|
||||||
msg_id,
|
|
||||||
chat_id,
|
|
||||||
accept_call_info,
|
|
||||||
} => OutgoingCallAccepted {
|
|
||||||
msg_id: msg_id.to_u32(),
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
accept_call_info,
|
|
||||||
},
|
|
||||||
CoreEventType::CallEnded { msg_id, chat_id } => CallEnded {
|
|
||||||
msg_id: msg_id.to_u32(),
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
},
|
|
||||||
CoreEventType::TransportsModified => TransportsModified,
|
|
||||||
|
|
||||||
#[allow(unreachable_patterns)]
|
#[allow(unreachable_patterns)]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||||
|
|||||||
@@ -4,16 +4,6 @@ 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`,
|
||||||
@@ -33,12 +23,6 @@ pub struct EnteredLoginParam {
|
|||||||
/// Imap server port.
|
/// Imap server port.
|
||||||
pub imap_port: Option<u16>,
|
pub imap_port: Option<u16>,
|
||||||
|
|
||||||
/// IMAP server folder.
|
|
||||||
///
|
|
||||||
/// Defaults to "INBOX" if not set.
|
|
||||||
/// Should not be an empty string.
|
|
||||||
pub imap_folder: Option<String>,
|
|
||||||
|
|
||||||
/// Imap socket security.
|
/// Imap socket security.
|
||||||
pub imap_security: Option<Socket>,
|
pub imap_security: Option<Socket>,
|
||||||
|
|
||||||
@@ -72,15 +56,6 @@ pub struct EnteredLoginParam {
|
|||||||
pub oauth2: Option<bool>,
|
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 {
|
||||||
fn from(param: dc::EnteredLoginParam) -> Self {
|
fn from(param: dc::EnteredLoginParam) -> Self {
|
||||||
let imap_security: Socket = param.imap.security.into();
|
let imap_security: Socket = param.imap.security.into();
|
||||||
@@ -91,7 +66,6 @@ impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
|||||||
password: param.imap.password,
|
password: param.imap.password,
|
||||||
imap_server: param.imap.server.into_option(),
|
imap_server: param.imap.server.into_option(),
|
||||||
imap_port: param.imap.port.into_option(),
|
imap_port: param.imap.port.into_option(),
|
||||||
imap_folder: param.imap.folder.into_option(),
|
|
||||||
imap_security: imap_security.into_option(),
|
imap_security: imap_security.into_option(),
|
||||||
imap_user: param.imap.user.into_option(),
|
imap_user: param.imap.user.into_option(),
|
||||||
smtp_server: param.smtp.server.into_option(),
|
smtp_server: param.smtp.server.into_option(),
|
||||||
@@ -111,15 +85,14 @@ impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
|
|||||||
fn try_from(param: EnteredLoginParam) -> Result<Self> {
|
fn try_from(param: EnteredLoginParam) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
addr: param.addr,
|
addr: param.addr,
|
||||||
imap: dc::EnteredImapLoginParam {
|
imap: dc::EnteredServerLoginParam {
|
||||||
server: param.imap_server.unwrap_or_default(),
|
server: param.imap_server.unwrap_or_default(),
|
||||||
port: param.imap_port.unwrap_or_default(),
|
port: param.imap_port.unwrap_or_default(),
|
||||||
folder: param.imap_folder.unwrap_or_default(),
|
|
||||||
security: param.imap_security.unwrap_or_default().into(),
|
security: param.imap_security.unwrap_or_default().into(),
|
||||||
user: param.imap_user.unwrap_or_default(),
|
user: param.imap_user.unwrap_or_default(),
|
||||||
password: param.password,
|
password: param.password,
|
||||||
},
|
},
|
||||||
smtp: dc::EnteredSmtpLoginParam {
|
smtp: dc::EnteredServerLoginParam {
|
||||||
server: param.smtp_server.unwrap_or_default(),
|
server: param.smtp_server.unwrap_or_default(),
|
||||||
port: param.smtp_port.unwrap_or_default(),
|
port: param.smtp_port.unwrap_or_default(),
|
||||||
security: param.smtp_security.unwrap_or_default().into(),
|
security: param.smtp_security.unwrap_or_default().into(),
|
||||||
|
|||||||
@@ -16,14 +16,13 @@ use num_traits::cast::ToPrimitive;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
use super::chat::JsonrpcChatType;
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
use super::contact::ContactObject;
|
use super::contact::ContactObject;
|
||||||
use super::reactions::JsonrpcReactions;
|
use super::reactions::JSONRPCReactions;
|
||||||
|
use super::webxdc::WebxdcMessageInfo;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||||
#[expect(clippy::large_enum_variant)]
|
|
||||||
pub enum MessageLoadResult {
|
pub enum MessageLoadResult {
|
||||||
Message(MessageObject),
|
Message(MessageObject),
|
||||||
LoadingError { error: String },
|
LoadingError { error: String },
|
||||||
@@ -60,14 +59,8 @@ pub struct MessageObject {
|
|||||||
|
|
||||||
// summary - use/create another function if you need it
|
// summary - use/create another function if you need it
|
||||||
subject: String,
|
subject: String,
|
||||||
|
|
||||||
/// True if the message was correctly encrypted&signed, false otherwise.
|
|
||||||
/// Historically, UIs showed a small padlock on the message then.
|
|
||||||
///
|
|
||||||
/// Today, the UIs should instead show a small email-icon on the message
|
|
||||||
/// if `show_padlock` is `false`,
|
|
||||||
/// and nothing if it is `true`.
|
|
||||||
show_padlock: bool,
|
show_padlock: bool,
|
||||||
|
is_setupmessage: bool,
|
||||||
is_info: bool,
|
is_info: bool,
|
||||||
is_forwarded: bool,
|
is_forwarded: bool,
|
||||||
|
|
||||||
@@ -84,17 +77,21 @@ pub struct MessageObject {
|
|||||||
dimensions_height: i32,
|
dimensions_height: i32,
|
||||||
dimensions_width: i32,
|
dimensions_width: i32,
|
||||||
|
|
||||||
|
videochat_type: Option<u32>,
|
||||||
|
videochat_url: Option<String>,
|
||||||
|
|
||||||
override_sender_name: Option<String>,
|
override_sender_name: Option<String>,
|
||||||
sender: ContactObject,
|
sender: ContactObject,
|
||||||
|
|
||||||
|
setup_code_begin: Option<String>,
|
||||||
|
|
||||||
file: Option<String>,
|
file: Option<String>,
|
||||||
file_mime: Option<String>,
|
file_mime: Option<String>,
|
||||||
|
|
||||||
/// The size of the file in bytes, if applicable.
|
|
||||||
/// If message is a pre-message, then this is the size of the file to be downloaded.
|
|
||||||
file_bytes: u64,
|
file_bytes: u64,
|
||||||
file_name: Option<String>,
|
file_name: Option<String>,
|
||||||
|
|
||||||
|
webxdc_info: Option<WebxdcMessageInfo>,
|
||||||
|
|
||||||
webxdc_href: Option<String>,
|
webxdc_href: Option<String>,
|
||||||
|
|
||||||
download_state: DownloadState,
|
download_state: DownloadState,
|
||||||
@@ -103,7 +100,7 @@ pub struct MessageObject {
|
|||||||
|
|
||||||
saved_message_id: Option<u32>,
|
saved_message_id: Option<u32>,
|
||||||
|
|
||||||
reactions: Option<JsonrpcReactions>,
|
reactions: Option<JSONRPCReactions>,
|
||||||
|
|
||||||
vcard_contact: Option<VcardContact>,
|
vcard_contact: Option<VcardContact>,
|
||||||
}
|
}
|
||||||
@@ -145,6 +142,12 @@ impl MessageObject {
|
|||||||
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
|
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
|
||||||
let override_sender_name = message.get_override_sender_name();
|
let override_sender_name = message.get_override_sender_name();
|
||||||
|
|
||||||
|
let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc {
|
||||||
|
Some(WebxdcMessageInfo::get_for_message(context, msg_id).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let parent_id = message.parent(context).await?.map(|m| m.get_id().to_u32());
|
let parent_id = message.parent(context).await?.map(|m| m.get_id().to_u32());
|
||||||
|
|
||||||
let download_state = message.download_state().into();
|
let download_state = message.download_state().into();
|
||||||
@@ -223,6 +226,7 @@ impl MessageObject {
|
|||||||
|
|
||||||
subject: message.get_subject().to_owned(),
|
subject: message.get_subject().to_owned(),
|
||||||
show_padlock: message.get_showpadlock(),
|
show_padlock: message.get_showpadlock(),
|
||||||
|
is_setupmessage: message.is_setupmessage(),
|
||||||
is_info: message.is_info(),
|
is_info: message.is_info(),
|
||||||
is_forwarded: message.is_forwarded(),
|
is_forwarded: message.is_forwarded(),
|
||||||
is_bot: message.is_bot(),
|
is_bot: message.is_bot(),
|
||||||
@@ -236,9 +240,20 @@ impl MessageObject {
|
|||||||
dimensions_height: message.get_height(),
|
dimensions_height: message.get_height(),
|
||||||
dimensions_width: message.get_width(),
|
dimensions_width: message.get_width(),
|
||||||
|
|
||||||
|
videochat_type: match message.get_videochat_type() {
|
||||||
|
Some(vct) => Some(
|
||||||
|
vct.to_u32()
|
||||||
|
.context("videochat type conversion to number failed")?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
videochat_url: message.get_videochat_url(),
|
||||||
|
|
||||||
override_sender_name,
|
override_sender_name,
|
||||||
sender,
|
sender,
|
||||||
|
|
||||||
|
setup_code_begin: message.get_setupcodebegin(context).await,
|
||||||
|
|
||||||
file: match message.get_file(context) {
|
file: match message.get_file(context) {
|
||||||
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,
|
||||||
@@ -246,6 +261,7 @@ impl MessageObject {
|
|||||||
file_mime: message.get_filemime(),
|
file_mime: message.get_filemime(),
|
||||||
file_bytes,
|
file_bytes,
|
||||||
file_name: message.get_filename(),
|
file_name: message.get_filename(),
|
||||||
|
webxdc_info,
|
||||||
|
|
||||||
// On a WebxdcInfoMessage this might include a hash holding
|
// On a WebxdcInfoMessage this might include a hash holding
|
||||||
// information about a specific position or state in a webxdc app
|
// information about a specific position or state in a webxdc app
|
||||||
@@ -287,6 +303,8 @@ pub enum MessageViewtype {
|
|||||||
Gif,
|
Gif,
|
||||||
|
|
||||||
/// Message containing a sticker, similar to image.
|
/// Message containing a sticker, similar to image.
|
||||||
|
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
|
||||||
|
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
|
||||||
///
|
///
|
||||||
/// If possible, the ui should display the image without borders in a transparent way.
|
/// If possible, the ui should display the image without borders in a transparent way.
|
||||||
/// A click on a sticker will offer to install the sticker set in some future.
|
/// A click on a sticker will offer to install the sticker set in some future.
|
||||||
@@ -305,8 +323,8 @@ pub enum MessageViewtype {
|
|||||||
/// Message containing any file, eg. a PDF.
|
/// Message containing any file, eg. a PDF.
|
||||||
File,
|
File,
|
||||||
|
|
||||||
/// Message is a call.
|
/// Message is an invitation to a videochat.
|
||||||
Call,
|
VideochatInvitation,
|
||||||
|
|
||||||
/// Message is an webxdc instance.
|
/// Message is an webxdc instance.
|
||||||
Webxdc,
|
Webxdc,
|
||||||
@@ -329,7 +347,7 @@ impl From<Viewtype> for MessageViewtype {
|
|||||||
Viewtype::Voice => MessageViewtype::Voice,
|
Viewtype::Voice => MessageViewtype::Voice,
|
||||||
Viewtype::Video => MessageViewtype::Video,
|
Viewtype::Video => MessageViewtype::Video,
|
||||||
Viewtype::File => MessageViewtype::File,
|
Viewtype::File => MessageViewtype::File,
|
||||||
Viewtype::Call => MessageViewtype::Call,
|
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
|
||||||
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
||||||
Viewtype::Vcard => MessageViewtype::Vcard,
|
Viewtype::Vcard => MessageViewtype::Vcard,
|
||||||
}
|
}
|
||||||
@@ -348,7 +366,7 @@ impl From<MessageViewtype> for Viewtype {
|
|||||||
MessageViewtype::Voice => Viewtype::Voice,
|
MessageViewtype::Voice => Viewtype::Voice,
|
||||||
MessageViewtype::Video => Viewtype::Video,
|
MessageViewtype::Video => Viewtype::Video,
|
||||||
MessageViewtype::File => Viewtype::File,
|
MessageViewtype::File => Viewtype::File,
|
||||||
MessageViewtype::Call => Viewtype::Call,
|
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
|
||||||
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
||||||
MessageViewtype::Vcard => Viewtype::Vcard,
|
MessageViewtype::Vcard => Viewtype::Vcard,
|
||||||
}
|
}
|
||||||
@@ -380,7 +398,6 @@ impl From<download::DownloadState> for DownloadState {
|
|||||||
pub enum SystemMessageType {
|
pub enum SystemMessageType {
|
||||||
Unknown,
|
Unknown,
|
||||||
GroupNameChanged,
|
GroupNameChanged,
|
||||||
GroupDescriptionChanged,
|
|
||||||
GroupImageChanged,
|
GroupImageChanged,
|
||||||
MemberAddedToGroup,
|
MemberAddedToGroup,
|
||||||
MemberRemovedFromGroup,
|
MemberRemovedFromGroup,
|
||||||
@@ -401,9 +418,6 @@ pub enum SystemMessageType {
|
|||||||
/// Chat ephemeral message timer is changed.
|
/// Chat ephemeral message timer is changed.
|
||||||
EphemeralTimerChanged,
|
EphemeralTimerChanged,
|
||||||
|
|
||||||
// Chat is e2ee
|
|
||||||
ChatE2ee,
|
|
||||||
|
|
||||||
// Chat protection state changed
|
// Chat protection state changed
|
||||||
ChatProtectionEnabled,
|
ChatProtectionEnabled,
|
||||||
ChatProtectionDisabled,
|
ChatProtectionDisabled,
|
||||||
@@ -422,9 +436,6 @@ pub enum SystemMessageType {
|
|||||||
|
|
||||||
/// This message contains a users iroh node address.
|
/// This message contains a users iroh node address.
|
||||||
IrohNodeAddr,
|
IrohNodeAddr,
|
||||||
|
|
||||||
CallAccepted,
|
|
||||||
CallEnded,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||||
@@ -433,7 +444,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
|||||||
match system_message_type {
|
match system_message_type {
|
||||||
SystemMessage::Unknown => SystemMessageType::Unknown,
|
SystemMessage::Unknown => SystemMessageType::Unknown,
|
||||||
SystemMessage::GroupNameChanged => SystemMessageType::GroupNameChanged,
|
SystemMessage::GroupNameChanged => SystemMessageType::GroupNameChanged,
|
||||||
SystemMessage::GroupDescriptionChanged => SystemMessageType::GroupDescriptionChanged,
|
|
||||||
SystemMessage::GroupImageChanged => SystemMessageType::GroupImageChanged,
|
SystemMessage::GroupImageChanged => SystemMessageType::GroupImageChanged,
|
||||||
SystemMessage::MemberAddedToGroup => SystemMessageType::MemberAddedToGroup,
|
SystemMessage::MemberAddedToGroup => SystemMessageType::MemberAddedToGroup,
|
||||||
SystemMessage::MemberRemovedFromGroup => SystemMessageType::MemberRemovedFromGroup,
|
SystemMessage::MemberRemovedFromGroup => SystemMessageType::MemberRemovedFromGroup,
|
||||||
@@ -442,7 +452,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
|||||||
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
|
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
|
||||||
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
|
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
|
||||||
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
|
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
|
||||||
SystemMessage::ChatE2ee => SystemMessageType::ChatE2ee,
|
|
||||||
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
|
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
|
||||||
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
|
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
|
||||||
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
||||||
@@ -452,8 +461,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
|||||||
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
|
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
|
||||||
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
|
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
|
||||||
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
|
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
|
||||||
SystemMessage::CallAccepted => SystemMessageType::CallAccepted,
|
|
||||||
SystemMessage::CallEnded => SystemMessageType::CallEnded,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -529,7 +536,8 @@ pub struct MessageSearchResult {
|
|||||||
chat_profile_image: Option<String>,
|
chat_profile_image: Option<String>,
|
||||||
chat_color: String,
|
chat_color: String,
|
||||||
chat_name: String,
|
chat_name: String,
|
||||||
chat_type: JsonrpcChatType,
|
chat_type: u32,
|
||||||
|
is_chat_protected: bool,
|
||||||
is_chat_contact_request: bool,
|
is_chat_contact_request: bool,
|
||||||
is_chat_archived: bool,
|
is_chat_archived: bool,
|
||||||
message: String,
|
message: String,
|
||||||
@@ -567,8 +575,9 @@ impl MessageSearchResult {
|
|||||||
chat_id: chat.id.to_u32(),
|
chat_id: chat.id.to_u32(),
|
||||||
chat_name: chat.get_name().to_owned(),
|
chat_name: chat.get_name().to_owned(),
|
||||||
chat_color,
|
chat_color,
|
||||||
chat_type: chat.get_type().into(),
|
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||||
chat_profile_image,
|
chat_profile_image,
|
||||||
|
is_chat_protected: chat.is_protected(),
|
||||||
is_chat_contact_request: chat.is_contact_request(),
|
is_chat_contact_request: chat.is_contact_request(),
|
||||||
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
|
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
|
||||||
message: message.get_text(),
|
message: message.get_text(),
|
||||||
@@ -579,7 +588,7 @@ impl MessageSearchResult {
|
|||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
|
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
|
||||||
pub enum JsonrpcMessageListItem {
|
pub enum JSONRPCMessageListItem {
|
||||||
Message {
|
Message {
|
||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
},
|
},
|
||||||
@@ -592,13 +601,13 @@ pub enum JsonrpcMessageListItem {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ChatItem> for JsonrpcMessageListItem {
|
impl From<ChatItem> for JSONRPCMessageListItem {
|
||||||
fn from(item: ChatItem) -> Self {
|
fn from(item: ChatItem) -> Self {
|
||||||
match item {
|
match item {
|
||||||
ChatItem::Message { msg_id } => JsonrpcMessageListItem::Message {
|
ChatItem::Message { msg_id } => JSONRPCMessageListItem::Message {
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
ChatItem::DayMarker { timestamp } => JsonrpcMessageListItem::DayMarker { timestamp },
|
ChatItem::DayMarker { timestamp } => JSONRPCMessageListItem::DayMarker { timestamp },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod account;
|
pub mod account;
|
||||||
pub mod calls;
|
|
||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod chat_list;
|
pub mod chat_list;
|
||||||
pub mod contact;
|
pub mod contact;
|
||||||
@@ -8,7 +7,6 @@ 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 provider_info;
|
||||||
pub mod qr;
|
pub mod qr;
|
||||||
pub mod reactions;
|
pub mod reactions;
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
use deltachat::qr::Qr;
|
use deltachat::qr::Qr;
|
||||||
use serde::Deserialize;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
@@ -19,8 +18,6 @@ pub enum QrObject {
|
|||||||
invitenumber: String,
|
invitenumber: String,
|
||||||
/// Authentication code.
|
/// Authentication code.
|
||||||
authcode: String,
|
authcode: String,
|
||||||
/// Whether the inviter supports the new Securejoin v3 protocol
|
|
||||||
is_v3: bool,
|
|
||||||
},
|
},
|
||||||
/// Ask the user whether to join the group.
|
/// Ask the user whether to join the group.
|
||||||
AskVerifyGroup {
|
AskVerifyGroup {
|
||||||
@@ -36,30 +33,6 @@ pub enum QrObject {
|
|||||||
invitenumber: String,
|
invitenumber: String,
|
||||||
/// Authentication code.
|
/// Authentication code.
|
||||||
authcode: String,
|
authcode: String,
|
||||||
/// Whether the inviter supports the new Securejoin v3 protocol
|
|
||||||
is_v3: bool,
|
|
||||||
},
|
|
||||||
/// Ask the user whether to join the broadcast channel.
|
|
||||||
AskJoinBroadcast {
|
|
||||||
/// The user-visible name of this broadcast channel
|
|
||||||
name: String,
|
|
||||||
/// A string of random characters,
|
|
||||||
/// uniquely identifying this broadcast channel across all databases/clients.
|
|
||||||
/// Called `grpid` for historic reasons:
|
|
||||||
/// The id of multi-user chats is always called `grpid` in the database
|
|
||||||
/// because groups were once the only multi-user chats.
|
|
||||||
grpid: String,
|
|
||||||
/// ID of the contact who owns the broadcast channel and created the QR code.
|
|
||||||
contact_id: u32,
|
|
||||||
/// Fingerprint of the broadcast channel owner's key as scanned from the QR code.
|
|
||||||
fingerprint: String,
|
|
||||||
|
|
||||||
/// Invite number.
|
|
||||||
invitenumber: String,
|
|
||||||
/// Authentication code.
|
|
||||||
authcode: String,
|
|
||||||
/// Whether the inviter supports the new Securejoin v3 protocol
|
|
||||||
is_v3: bool,
|
|
||||||
},
|
},
|
||||||
/// Contact fingerprint is verified.
|
/// Contact fingerprint is verified.
|
||||||
///
|
///
|
||||||
@@ -163,21 +136,6 @@ pub enum QrObject {
|
|||||||
/// Authentication code.
|
/// Authentication code.
|
||||||
authcode: String,
|
authcode: String,
|
||||||
},
|
},
|
||||||
/// Ask the user if they want to withdraw their own broadcast channel invite QR code.
|
|
||||||
WithdrawJoinBroadcast {
|
|
||||||
/// Broadcast name.
|
|
||||||
name: String,
|
|
||||||
/// ID, uniquely identifying this chat. Called grpid for historic reasons.
|
|
||||||
grpid: String,
|
|
||||||
/// Contact ID. Always `ContactId::SELF`.
|
|
||||||
contact_id: u32,
|
|
||||||
/// Fingerprint of the contact key as scanned from the QR code.
|
|
||||||
fingerprint: String,
|
|
||||||
/// Invite number.
|
|
||||||
invitenumber: String,
|
|
||||||
/// Authentication code.
|
|
||||||
authcode: String,
|
|
||||||
},
|
|
||||||
/// Ask the user if they want to revive their own QR code.
|
/// Ask the user if they want to revive their own QR code.
|
||||||
ReviveVerifyContact {
|
ReviveVerifyContact {
|
||||||
/// Contact ID.
|
/// Contact ID.
|
||||||
@@ -204,21 +162,6 @@ pub enum QrObject {
|
|||||||
/// Authentication code.
|
/// Authentication code.
|
||||||
authcode: String,
|
authcode: String,
|
||||||
},
|
},
|
||||||
/// Ask the user if they want to revive their own broadcast channel invite QR code.
|
|
||||||
ReviveJoinBroadcast {
|
|
||||||
/// Broadcast name.
|
|
||||||
name: String,
|
|
||||||
/// Globally unique chat ID. Called grpid for historic reasons.
|
|
||||||
grpid: String,
|
|
||||||
/// Contact ID. Always `ContactId::SELF`.
|
|
||||||
contact_id: u32,
|
|
||||||
/// Fingerprint of the contact key as scanned from the QR code.
|
|
||||||
fingerprint: String,
|
|
||||||
/// Invite number.
|
|
||||||
invitenumber: String,
|
|
||||||
/// Authentication code.
|
|
||||||
authcode: String,
|
|
||||||
},
|
|
||||||
/// `dclogin:` scheme parameters.
|
/// `dclogin:` scheme parameters.
|
||||||
///
|
///
|
||||||
/// Ask the user if they want to login with the email address.
|
/// Ask the user if they want to login with the email address.
|
||||||
@@ -235,16 +178,14 @@ impl From<Qr> for QrObject {
|
|||||||
fingerprint,
|
fingerprint,
|
||||||
invitenumber,
|
invitenumber,
|
||||||
authcode,
|
authcode,
|
||||||
is_v3,
|
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.human_readable();
|
let fingerprint = fingerprint.to_string();
|
||||||
QrObject::AskVerifyContact {
|
QrObject::AskVerifyContact {
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
invitenumber,
|
invitenumber,
|
||||||
authcode,
|
authcode,
|
||||||
is_v3,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Qr::AskVerifyGroup {
|
Qr::AskVerifyGroup {
|
||||||
@@ -254,10 +195,9 @@ impl From<Qr> for QrObject {
|
|||||||
fingerprint,
|
fingerprint,
|
||||||
invitenumber,
|
invitenumber,
|
||||||
authcode,
|
authcode,
|
||||||
is_v3,
|
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.human_readable();
|
let fingerprint = fingerprint.to_string();
|
||||||
QrObject::AskVerifyGroup {
|
QrObject::AskVerifyGroup {
|
||||||
grpname,
|
grpname,
|
||||||
grpid,
|
grpid,
|
||||||
@@ -265,28 +205,6 @@ impl From<Qr> for QrObject {
|
|||||||
fingerprint,
|
fingerprint,
|
||||||
invitenumber,
|
invitenumber,
|
||||||
authcode,
|
authcode,
|
||||||
is_v3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Qr::AskJoinBroadcast {
|
|
||||||
name,
|
|
||||||
grpid,
|
|
||||||
contact_id,
|
|
||||||
fingerprint,
|
|
||||||
authcode,
|
|
||||||
invitenumber,
|
|
||||||
is_v3,
|
|
||||||
} => {
|
|
||||||
let contact_id = contact_id.to_u32();
|
|
||||||
let fingerprint = fingerprint.human_readable();
|
|
||||||
QrObject::AskJoinBroadcast {
|
|
||||||
name,
|
|
||||||
grpid,
|
|
||||||
contact_id,
|
|
||||||
fingerprint,
|
|
||||||
authcode,
|
|
||||||
invitenumber,
|
|
||||||
is_v3,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Qr::FprOk { contact_id } => {
|
Qr::FprOk { contact_id } => {
|
||||||
@@ -307,6 +225,13 @@ impl From<Qr> for QrObject {
|
|||||||
auth_token,
|
auth_token,
|
||||||
},
|
},
|
||||||
Qr::BackupTooNew {} => QrObject::BackupTooNew {},
|
Qr::BackupTooNew {} => QrObject::BackupTooNew {},
|
||||||
|
Qr::WebrtcInstance {
|
||||||
|
domain,
|
||||||
|
instance_pattern,
|
||||||
|
} => QrObject::WebrtcInstance {
|
||||||
|
domain,
|
||||||
|
instance_pattern,
|
||||||
|
},
|
||||||
Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port },
|
Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port },
|
||||||
Qr::Addr { contact_id, draft } => {
|
Qr::Addr { contact_id, draft } => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
@@ -321,7 +246,7 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.human_readable();
|
let fingerprint = fingerprint.to_string();
|
||||||
QrObject::WithdrawVerifyContact {
|
QrObject::WithdrawVerifyContact {
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
@@ -338,7 +263,7 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.human_readable();
|
let fingerprint = fingerprint.to_string();
|
||||||
QrObject::WithdrawVerifyGroup {
|
QrObject::WithdrawVerifyGroup {
|
||||||
grpname,
|
grpname,
|
||||||
grpid,
|
grpid,
|
||||||
@@ -348,25 +273,6 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Qr::WithdrawJoinBroadcast {
|
|
||||||
name,
|
|
||||||
grpid,
|
|
||||||
contact_id,
|
|
||||||
fingerprint,
|
|
||||||
invitenumber,
|
|
||||||
authcode,
|
|
||||||
} => {
|
|
||||||
let contact_id = contact_id.to_u32();
|
|
||||||
let fingerprint = fingerprint.human_readable();
|
|
||||||
QrObject::WithdrawJoinBroadcast {
|
|
||||||
name,
|
|
||||||
grpid,
|
|
||||||
contact_id,
|
|
||||||
fingerprint,
|
|
||||||
invitenumber,
|
|
||||||
authcode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Qr::ReviveVerifyContact {
|
Qr::ReviveVerifyContact {
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
@@ -374,7 +280,7 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.human_readable();
|
let fingerprint = fingerprint.to_string();
|
||||||
QrObject::ReviveVerifyContact {
|
QrObject::ReviveVerifyContact {
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
@@ -391,7 +297,7 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.human_readable();
|
let fingerprint = fingerprint.to_string();
|
||||||
QrObject::ReviveVerifyGroup {
|
QrObject::ReviveVerifyGroup {
|
||||||
grpname,
|
grpname,
|
||||||
grpid,
|
grpid,
|
||||||
@@ -401,76 +307,7 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Qr::ReviveJoinBroadcast {
|
|
||||||
name,
|
|
||||||
grpid,
|
|
||||||
contact_id,
|
|
||||||
fingerprint,
|
|
||||||
invitenumber,
|
|
||||||
authcode,
|
|
||||||
} => {
|
|
||||||
let contact_id = contact_id.to_u32();
|
|
||||||
let fingerprint = fingerprint.human_readable();
|
|
||||||
QrObject::ReviveJoinBroadcast {
|
|
||||||
name,
|
|
||||||
grpid,
|
|
||||||
contact_id,
|
|
||||||
fingerprint,
|
|
||||||
invitenumber,
|
|
||||||
authcode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Qr::Login { address, .. } => QrObject::Login { address },
|
Qr::Login { address, .. } => QrObject::Login { address },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, TypeDef, schemars::JsonSchema)]
|
|
||||||
pub enum SecurejoinSource {
|
|
||||||
/// Because of some problem, it is unknown where the QR code came from.
|
|
||||||
Unknown,
|
|
||||||
/// The user opened a link somewhere outside Delta Chat
|
|
||||||
ExternalLink,
|
|
||||||
/// The user clicked on a link in a message inside Delta Chat
|
|
||||||
InternalLink,
|
|
||||||
/// The user clicked "Paste from Clipboard" in the QR scan activity
|
|
||||||
Clipboard,
|
|
||||||
/// The user clicked "Load QR code as image" in the QR scan activity
|
|
||||||
ImageLoaded,
|
|
||||||
/// The user scanned a QR code
|
|
||||||
Scan,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, TypeDef, schemars::JsonSchema)]
|
|
||||||
pub enum SecurejoinUiPath {
|
|
||||||
/// The UI path is unknown, or the user didn't open the QR code screen at all.
|
|
||||||
Unknown,
|
|
||||||
/// The user directly clicked on the QR icon in the main screen
|
|
||||||
QrIcon,
|
|
||||||
/// The user first clicked on the `+` button in the main screen,
|
|
||||||
/// and then on "New Contact"
|
|
||||||
NewContact,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SecurejoinSource> for deltachat::SecurejoinSource {
|
|
||||||
fn from(value: SecurejoinSource) -> Self {
|
|
||||||
match value {
|
|
||||||
SecurejoinSource::Unknown => deltachat::SecurejoinSource::Unknown,
|
|
||||||
SecurejoinSource::ExternalLink => deltachat::SecurejoinSource::ExternalLink,
|
|
||||||
SecurejoinSource::InternalLink => deltachat::SecurejoinSource::InternalLink,
|
|
||||||
SecurejoinSource::Clipboard => deltachat::SecurejoinSource::Clipboard,
|
|
||||||
SecurejoinSource::ImageLoaded => deltachat::SecurejoinSource::ImageLoaded,
|
|
||||||
SecurejoinSource::Scan => deltachat::SecurejoinSource::Scan,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SecurejoinUiPath> for deltachat::SecurejoinUiPath {
|
|
||||||
fn from(value: SecurejoinUiPath) -> Self {
|
|
||||||
match value {
|
|
||||||
SecurejoinUiPath::Unknown => deltachat::SecurejoinUiPath::Unknown,
|
|
||||||
SecurejoinUiPath::QrIcon => deltachat::SecurejoinUiPath::QrIcon,
|
|
||||||
SecurejoinUiPath::NewContact => deltachat::SecurejoinUiPath::NewContact,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use typescript_type_def::TypeDef;
|
|||||||
/// A single reaction emoji.
|
/// A single reaction emoji.
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Reaction", rename_all = "camelCase")]
|
#[serde(rename = "Reaction", rename_all = "camelCase")]
|
||||||
pub struct JsonrpcReaction {
|
pub struct JSONRPCReaction {
|
||||||
/// Emoji.
|
/// Emoji.
|
||||||
emoji: String,
|
emoji: String,
|
||||||
|
|
||||||
@@ -22,32 +22,41 @@ pub struct JsonrpcReaction {
|
|||||||
/// Structure representing all reactions to a particular message.
|
/// Structure representing all reactions to a particular message.
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[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,
|
|
||||||
/// but this contains a list of reactions for historical reasons.
|
|
||||||
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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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 mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
|
||||||
.iter()
|
|
||||||
.map(|(key, value)| (key.to_u32(), vec![value.as_str().to_string()]))
|
for contact_id in reactions.contacts() {
|
||||||
.collect();
|
let reaction = reactions.get(contact_id);
|
||||||
let self_reaction = reactions_by_contact.get(&ContactId::SELF.to_u32());
|
if reaction.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let emojis: Vec<String> = reaction
|
||||||
|
.emojis()
|
||||||
|
.into_iter()
|
||||||
|
.map(|emoji| emoji.to_owned())
|
||||||
|
.collect();
|
||||||
|
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let self_reactions = reactions_by_contact.get(&ContactId::SELF.to_u32());
|
||||||
|
|
||||||
let mut reactions_v = Vec::new();
|
let mut reactions_v = Vec::new();
|
||||||
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
|
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
|
||||||
let is_from_self = if let Some(self_reaction) = self_reaction {
|
let is_from_self = if let Some(self_reactions) = self_reactions {
|
||||||
self_reaction.contains(&emoji)
|
self_reactions.contains(&emoji)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
let reaction = JsonrpcReaction {
|
let reaction = JSONRPCReaction {
|
||||||
emoji,
|
emoji,
|
||||||
count,
|
count,
|
||||||
is_from_self,
|
is_from_self,
|
||||||
@@ -55,7 +64,7 @@ impl From<Reactions> for JsonrpcReactions {
|
|||||||
reactions_v.push(reaction)
|
reactions_v.push(reaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonrpcReactions {
|
JSONRPCReactions {
|
||||||
reactions_by_contact,
|
reactions_by_contact,
|
||||||
reactions: reactions_v,
|
reactions: reactions_v,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,6 @@ pub struct WebxdcMessageInfo {
|
|||||||
internet_access: bool,
|
internet_access: bool,
|
||||||
/// Address to be used for `window.webxdc.selfAddr` in JS land.
|
/// Address to be used for `window.webxdc.selfAddr` in JS land.
|
||||||
self_addr: String,
|
self_addr: String,
|
||||||
/// Define if the local user is the one who initially shared the webxdc application in the chat.
|
|
||||||
is_app_sender: bool,
|
|
||||||
/// Define if the app runs in a broadcasting context.
|
|
||||||
is_broadcast: bool,
|
|
||||||
/// Milliseconds to wait before calling `sendUpdate()` again since the last call.
|
/// Milliseconds to wait before calling `sendUpdate()` again since the last call.
|
||||||
/// Should be exposed to `window.sendUpdateInterval` in JS land.
|
/// Should be exposed to `window.sendUpdateInterval` in JS land.
|
||||||
send_update_interval: usize,
|
send_update_interval: usize,
|
||||||
@@ -64,8 +60,6 @@ impl WebxdcMessageInfo {
|
|||||||
request_integration: _,
|
request_integration: _,
|
||||||
internet_access,
|
internet_access,
|
||||||
self_addr,
|
self_addr,
|
||||||
is_app_sender,
|
|
||||||
is_broadcast,
|
|
||||||
send_update_interval,
|
send_update_interval,
|
||||||
send_update_max_size,
|
send_update_max_size,
|
||||||
} = message.get_webxdc_info(context).await?;
|
} = message.get_webxdc_info(context).await?;
|
||||||
@@ -78,8 +72,6 @@ impl WebxdcMessageInfo {
|
|||||||
source_code_url: maybe_empty_string_to_option(source_code_url),
|
source_code_url: maybe_empty_string_to_option(source_code_url),
|
||||||
internet_access,
|
internet_access,
|
||||||
self_addr,
|
self_addr,
|
||||||
is_app_sender,
|
|
||||||
is_broadcast,
|
|
||||||
send_update_interval,
|
send_update_interval,
|
||||||
send_update_max_size,
|
send_update_max_size,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ mod tests {
|
|||||||
assert_eq!(result, response.to_owned());
|
assert_eq!(result, response.to_owned());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":""}]}"#;
|
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":""}]}"#;
|
||||||
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
|
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
|
||||||
session.handle_incoming(request).await;
|
session.handle_incoming(request).await;
|
||||||
let result = receiver.recv().await?;
|
let result = receiver.recv().await?;
|
||||||
|
|||||||
@@ -2,24 +2,24 @@
|
|||||||
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
|
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@deltachat/tiny-emitter": "3.0.0",
|
"@deltachat/tiny-emitter": "3.0.0",
|
||||||
"isomorphic-ws": "^5.0.0",
|
"isomorphic-ws": "^4.0.1",
|
||||||
"yerpc": "^0.6.2"
|
"yerpc": "^0.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.3.10",
|
"@types/chai": "^4.2.21",
|
||||||
"@types/chai-as-promised": "^7.1.8",
|
"@types/chai-as-promised": "^7.1.5",
|
||||||
"@types/mocha": "^10.0.4",
|
"@types/mocha": "^9.0.0",
|
||||||
"@types/ws": "^8.5.9",
|
"@types/ws": "^7.2.4",
|
||||||
"c8": "^8.0.1",
|
"c8": "^7.10.0",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"esbuild": "^0.25.5",
|
"esbuild": "^0.17.9",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"mocha": "^10.2.0",
|
"mocha": "^9.1.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^2.6.2",
|
||||||
"typedoc": "^0.28.5",
|
"typedoc": "^0.23.2",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^4.5.5",
|
||||||
"ws": "^8.5.0"
|
"ws": "^8.5.0"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -54,5 +54,5 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "dist/deltachat.d.ts",
|
"types": "dist/deltachat.d.ts",
|
||||||
"version": "2.50.0-dev"
|
"version": "1.159.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,24 +5,24 @@ const json = JSON.parse(readFileSync("./coverage/coverage-final.json"));
|
|||||||
const jsonCoverage =
|
const jsonCoverage =
|
||||||
json[Object.keys(json).find((k) => k.includes(generatedFile))];
|
json[Object.keys(json).find((k) => k.includes(generatedFile))];
|
||||||
const fnMap = Object.keys(jsonCoverage.fnMap).map(
|
const fnMap = Object.keys(jsonCoverage.fnMap).map(
|
||||||
(key) => jsonCoverage.fnMap[key],
|
(key) => jsonCoverage.fnMap[key]
|
||||||
);
|
);
|
||||||
const htmlCoverage = readFileSync(
|
const htmlCoverage = readFileSync(
|
||||||
"./coverage/" + generatedFile + ".html",
|
"./coverage/" + generatedFile + ".html",
|
||||||
"utf8",
|
"utf8"
|
||||||
);
|
);
|
||||||
const uncoveredLines = htmlCoverage
|
const uncoveredLines = htmlCoverage
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => line.includes(`"function not covered"`));
|
.filter((line) => line.includes(`"function not covered"`));
|
||||||
const uncoveredFunctions = uncoveredLines.map(
|
const uncoveredFunctions = uncoveredLines.map(
|
||||||
(line) => />([\w_]+)\(/.exec(line)[1],
|
(line) => />([\w_]+)\(/.exec(line)[1]
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
"\nUncovered api functions:\n" +
|
"\nUncovered api functions:\n" +
|
||||||
uncoveredFunctions
|
uncoveredFunctions
|
||||||
.map((uF) => fnMap.find(({ name }) => name === uF))
|
.map((uF) => fnMap.find(({ name }) => name === uF))
|
||||||
.map(
|
.map(
|
||||||
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`,
|
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`
|
||||||
)
|
)
|
||||||
.join("\n"),
|
.join("\n")
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ while (null != (match = regex.exec(header_data))) {
|
|||||||
|
|
||||||
const constants = data
|
const constants = data
|
||||||
.filter(
|
.filter(
|
||||||
({ key }) => key.toUpperCase()[0] === key[0], // check if define name is uppercase
|
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
|
||||||
)
|
)
|
||||||
.sort((lhs, rhs) => {
|
.sort((lhs, rhs) => {
|
||||||
if (lhs.key < rhs.key) return -1;
|
if (lhs.key < rhs.key) return -1;
|
||||||
@@ -40,35 +40,15 @@ const constants = data
|
|||||||
key.startsWith("DC_DOWNLOAD") ||
|
key.startsWith("DC_DOWNLOAD") ||
|
||||||
key.startsWith("DC_INFO_") ||
|
key.startsWith("DC_INFO_") ||
|
||||||
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
|
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
|
||||||
key.startsWith("DC_QR_") ||
|
key.startsWith("DC_QR_")
|
||||||
key.startsWith("DC_CERTCK_") ||
|
|
||||||
key.startsWith("DC_SOCKET_") ||
|
|
||||||
key.startsWith("DC_LP_AUTH_") ||
|
|
||||||
key.startsWith("DC_PUSH_") ||
|
|
||||||
key.startsWith("DC_TEXT1_") ||
|
|
||||||
key.startsWith("DC_CHAT_TYPE")
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map((row) => {
|
.map((row) => {
|
||||||
return ` export const ${row.key} = ${row.value};`;
|
return ` ${row.key}: ${row.value}`;
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join(",\n");
|
||||||
|
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
resolve(__dirname, "../generated/constants.ts"),
|
resolve(__dirname, "../generated/constants.ts"),
|
||||||
`// Generated!
|
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`
|
||||||
|
|
||||||
export namespace C {
|
|
||||||
${constants}
|
|
||||||
/** @deprecated 10-8-2025 compare string directly with \`== "Group"\` */
|
|
||||||
export const DC_CHAT_TYPE_GROUP = "Group";
|
|
||||||
/** @deprecated 10-8-2025 compare string directly with \`== "InBroadcast"\`*/
|
|
||||||
export const DC_CHAT_TYPE_IN_BROADCAST = "InBroadcast";
|
|
||||||
/** @deprecated 10-8-2025 compare string directly with \`== "Mailinglist"\` */
|
|
||||||
export const DC_CHAT_TYPE_MAILINGLIST = "Mailinglist";
|
|
||||||
/** @deprecated 10-8-2025 compare string directly with \`== "OutBroadcast"\` */
|
|
||||||
export const DC_CHAT_TYPE_OUT_BROADCAST = "OutBroadcast";
|
|
||||||
/** @deprecated 10-8-2025 compare string directly with \`== "Single"\` */
|
|
||||||
export const DC_CHAT_TYPE_SINGLE = "Single";
|
|
||||||
}\n`,
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import { TinyEmitter } from "@deltachat/tiny-emitter";
|
|||||||
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||||
[Property in EventType["kind"]]: (
|
[Property in EventType["kind"]]: (
|
||||||
accountId: number,
|
accountId: number,
|
||||||
event: Extract<EventType, { kind: Property }>,
|
event: Extract<EventType, { kind: Property }>
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContextEvents = { ALL: (event: EventType) => void } & {
|
type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||||
[Property in EventType["kind"]]: (
|
[Property in EventType["kind"]]: (
|
||||||
event: Extract<EventType, { kind: Property }>,
|
event: Extract<EventType, { kind: Property }>
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,22 +25,16 @@ export type DcEventType<T extends EventType["kind"]> = Extract<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export class BaseDeltaChat<
|
export class BaseDeltaChat<
|
||||||
Transport extends BaseTransport<any>,
|
Transport extends BaseTransport<any>
|
||||||
> extends TinyEmitter<Events> {
|
> extends TinyEmitter<Events> {
|
||||||
rpc: RawClient;
|
rpc: RawClient;
|
||||||
|
account?: T.Account;
|
||||||
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
private eventTask: Promise<void>;
|
private eventTask: Promise<void>;
|
||||||
|
|
||||||
constructor(
|
constructor(public transport: Transport, startEventLoop: boolean) {
|
||||||
public transport: Transport,
|
|
||||||
/**
|
|
||||||
* Whether to start calling {@linkcode RawClient.getNextEvent}
|
|
||||||
* and emitting the respective events on this class.
|
|
||||||
*/
|
|
||||||
startEventLoop: boolean,
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
this.rpc = new RawClient(this.transport);
|
this.rpc = new RawClient(this.transport);
|
||||||
if (startEventLoop) {
|
if (startEventLoop) {
|
||||||
@@ -48,39 +42,28 @@ export class BaseDeltaChat<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @see the constructor's `startEventLoop`
|
|
||||||
*/
|
|
||||||
async eventLoop(): Promise<void> {
|
async eventLoop(): Promise<void> {
|
||||||
while (true) {
|
while (true) {
|
||||||
for (const event of await this.rpc.getNextEventBatch()) {
|
const event = await this.rpc.getNextEvent();
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
this.emit(event.event.kind, event.contextId, event.event);
|
this.emit(event.event.kind, event.contextId, event.event);
|
||||||
this.emit("ALL", event.contextId, event.event);
|
this.emit("ALL", event.contextId, event.event);
|
||||||
|
|
||||||
if (this.contextEmitters[event.contextId]) {
|
if (this.contextEmitters[event.contextId]) {
|
||||||
this.contextEmitters[event.contextId].emit(
|
this.contextEmitters[event.contextId].emit(
|
||||||
event.event.kind,
|
event.event.kind,
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
event.event as any,
|
event.event as any
|
||||||
);
|
);
|
||||||
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated use {@linkcode BaseDeltaChat.rpc.getAllAccounts} instead.
|
|
||||||
*/
|
|
||||||
async listAccounts(): Promise<T.Account[]> {
|
async listAccounts(): Promise<T.Account[]> {
|
||||||
return await this.rpc.getAllAccounts();
|
return await this.rpc.getAllAccounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A convenience function to listen on events binned by `account_id`
|
|
||||||
* (see {@linkcode RawClient.getAllAccounts}).
|
|
||||||
*/
|
|
||||||
getContextEvents(account_id: number) {
|
getContextEvents(account_id: number) {
|
||||||
if (this.contextEmitters[account_id]) {
|
if (this.contextEmitters[account_id]) {
|
||||||
return this.contextEmitters[account_id];
|
return this.contextEmitters[account_id];
|
||||||
@@ -100,10 +83,7 @@ export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class StdioTransport extends BaseTransport {
|
export class StdioTransport extends BaseTransport {
|
||||||
constructor(
|
constructor(public input: any, public output: any) {
|
||||||
public input: any,
|
|
||||||
public output: any,
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
|
|
||||||
var buffer = "";
|
var buffer = "";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { strictEqual } from "assert";
|
||||||
import chai, { assert, expect } from "chai";
|
import chai, { assert, expect } from "chai";
|
||||||
import chaiAsPromised from "chai-as-promised";
|
import chaiAsPromised from "chai-as-promised";
|
||||||
chai.use(chaiAsPromised);
|
chai.use(chaiAsPromised);
|
||||||
@@ -31,14 +32,14 @@ describe("basic tests", () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
validAddresses.map((email) => dc.rpc.checkEmailValidity(email)),
|
validAddresses.map((email) => dc.rpc.checkEmailValidity(email))
|
||||||
),
|
)
|
||||||
).to.not.contain(false);
|
).to.not.contain(false);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email)),
|
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email))
|
||||||
),
|
)
|
||||||
).to.not.contain(true);
|
).to.not.contain(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ describe("basic tests", () => {
|
|||||||
const contactId = await dc.rpc.createContact(
|
const contactId = await dc.rpc.createContact(
|
||||||
accountId,
|
accountId,
|
||||||
"example@delta.chat",
|
"example@delta.chat",
|
||||||
null,
|
null
|
||||||
);
|
);
|
||||||
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
|
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
|
||||||
.false;
|
.false;
|
||||||
@@ -126,7 +127,7 @@ describe("basic tests", () => {
|
|||||||
await dc.rpc.batchSetConfig(accountId, config);
|
await dc.rpc.batchSetConfig(accountId, config);
|
||||||
const retrieved = await dc.rpc.batchGetConfig(
|
const retrieved = await dc.rpc.batchGetConfig(
|
||||||
accountId,
|
accountId,
|
||||||
Object.keys(config),
|
Object.keys(config)
|
||||||
);
|
);
|
||||||
expect(retrieved).to.deep.equal(config);
|
expect(retrieved).to.deep.equal(config);
|
||||||
});
|
});
|
||||||
@@ -138,7 +139,7 @@ describe("basic tests", () => {
|
|||||||
await dc.rpc.batchSetConfig(accountId, config);
|
await dc.rpc.batchSetConfig(accountId, config);
|
||||||
const retrieved = await dc.rpc.batchGetConfig(
|
const retrieved = await dc.rpc.batchGetConfig(
|
||||||
accountId,
|
accountId,
|
||||||
Object.keys(config),
|
Object.keys(config)
|
||||||
);
|
);
|
||||||
expect(retrieved).to.deep.equal(config);
|
expect(retrieved).to.deep.equal(config);
|
||||||
});
|
});
|
||||||
@@ -152,7 +153,7 @@ describe("basic tests", () => {
|
|||||||
await dc.rpc.batchSetConfig(accountId, config);
|
await dc.rpc.batchSetConfig(accountId, config);
|
||||||
const retrieved = await dc.rpc.batchGetConfig(
|
const retrieved = await dc.rpc.batchGetConfig(
|
||||||
accountId,
|
accountId,
|
||||||
Object.keys(config),
|
Object.keys(config)
|
||||||
);
|
);
|
||||||
expect(retrieved).to.deep.equal(config);
|
expect(retrieved).to.deep.equal(config);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ describe("online tests", function () {
|
|||||||
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
|
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
|
||||||
console.error(
|
console.error(
|
||||||
"CAN NOT RUN COVERAGE correctly: Missing CHATMAIL_DOMAIN environment variable!\n\n",
|
"CAN NOT RUN COVERAGE correctly: Missing CHATMAIL_DOMAIN environment variable!\n\n",
|
||||||
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test",
|
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test"
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests",
|
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests"
|
||||||
);
|
);
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ describe("online tests", function () {
|
|||||||
account1 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
account1 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
||||||
if (!account1 || !account1.email || !account1.password) {
|
if (!account1 || !account1.email || !account1.password) {
|
||||||
console.log(
|
console.log(
|
||||||
"We didn't got back an account from the api, skip integration tests",
|
"We didn't got back an account from the api, skip integration tests"
|
||||||
);
|
);
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ describe("online tests", function () {
|
|||||||
account2 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
account2 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
||||||
if (!account2 || !account2.email || !account2.password) {
|
if (!account2 || !account2.email || !account2.password) {
|
||||||
console.log(
|
console.log(
|
||||||
"We didn't got back an account2 from the api, skip integration tests",
|
"We didn't got back an account2 from the api, skip integration tests"
|
||||||
);
|
);
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,6 @@ describe("online tests", function () {
|
|||||||
await dc.rpc.setConfig(accountId1, "addr", account1.email);
|
await dc.rpc.setConfig(accountId1, "addr", account1.email);
|
||||||
await dc.rpc.setConfig(accountId1, "mail_pw", account1.password);
|
await dc.rpc.setConfig(accountId1, "mail_pw", account1.password);
|
||||||
await dc.rpc.configure(accountId1);
|
await dc.rpc.configure(accountId1);
|
||||||
await waitForEvent(dc, "ImapInboxIdle", accountId1);
|
|
||||||
|
|
||||||
accountId2 = await dc.rpc.addAccount();
|
accountId2 = await dc.rpc.addAccount();
|
||||||
await dc.rpc.batchSetConfig(accountId2, {
|
await dc.rpc.batchSetConfig(accountId2, {
|
||||||
@@ -72,7 +71,6 @@ describe("online tests", function () {
|
|||||||
mail_pw: account2.password,
|
mail_pw: account2.password,
|
||||||
});
|
});
|
||||||
await dc.rpc.configure(accountId2);
|
await dc.rpc.configure(accountId2);
|
||||||
await waitForEvent(dc, "ImapInboxIdle", accountId2);
|
|
||||||
accountsConfigured = true;
|
accountsConfigured = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,13 +92,11 @@ describe("online tests", function () {
|
|||||||
accountId2,
|
accountId2,
|
||||||
chatIdOnAccountB,
|
chatIdOnAccountB,
|
||||||
false,
|
false,
|
||||||
false,
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
// There are 2 messages in the chat:
|
expect(messageList).have.length(1);
|
||||||
// 'Messages are end-to-end encrypted' (info message) and 'Hello'
|
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
|
||||||
expect(messageList).have.length(2);
|
|
||||||
const message = await dc.rpc.getMessage(accountId2, messageList[1]);
|
|
||||||
expect(message.text).equal("Hello");
|
expect(message.text).equal("Hello");
|
||||||
expect(message.showPadlock).equal(true);
|
expect(message.showPadlock).equal(true);
|
||||||
});
|
});
|
||||||
@@ -128,11 +124,11 @@ describe("online tests", function () {
|
|||||||
accountId2,
|
accountId2,
|
||||||
chatIdOnAccountB,
|
chatIdOnAccountB,
|
||||||
false,
|
false,
|
||||||
false,
|
false
|
||||||
);
|
);
|
||||||
const message = await dc.rpc.getMessage(
|
const message = await dc.rpc.getMessage(
|
||||||
accountId2,
|
accountId2,
|
||||||
messageList.reverse()[0],
|
messageList.reverse()[0]
|
||||||
);
|
);
|
||||||
expect(message.text).equal("Hello2");
|
expect(message.text).equal("Hello2");
|
||||||
// Send message back from B to A
|
// Send message back from B to A
|
||||||
@@ -154,7 +150,7 @@ describe("online tests", function () {
|
|||||||
const info = await dc.rpc.getProviderInfo(acc, "example.com");
|
const info = await dc.rpc.getProviderInfo(acc, "example.com");
|
||||||
expect(info).to.be.not.null;
|
expect(info).to.be.not.null;
|
||||||
expect(info?.overviewPage).to.equal(
|
expect(info?.overviewPage).to.equal(
|
||||||
"https://providers.delta.chat/example-com",
|
"https://providers.delta.chat/example-com"
|
||||||
);
|
);
|
||||||
expect(info?.status).to.equal(3);
|
expect(info?.status).to.equal(3);
|
||||||
});
|
});
|
||||||
@@ -171,12 +167,12 @@ async function waitForEvent<T extends DcEvent["kind"]>(
|
|||||||
dc: DeltaChat,
|
dc: DeltaChat,
|
||||||
eventType: T,
|
eventType: T,
|
||||||
accountId: number,
|
accountId: number,
|
||||||
timeout: number = EVENT_TIMEOUT,
|
timeout: number = EVENT_TIMEOUT
|
||||||
): Promise<Extract<DcEvent, { kind: T }>> {
|
): Promise<Extract<DcEvent, { kind: T }>> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const rejectTimeout = setTimeout(
|
const rejectTimeout = setTimeout(
|
||||||
() => reject(new Error("Timeout reached before event came in")),
|
() => reject(new Error("Timeout reached before event came in")),
|
||||||
timeout,
|
timeout
|
||||||
);
|
);
|
||||||
const callback = (contextId: number, event: DcEvent) => {
|
const callback = (contextId: number, event: DcEvent) => {
|
||||||
if (contextId == accountId) {
|
if (contextId == accountId) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export async function startServer(): Promise<RpcServerHandle> {
|
|||||||
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
|
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
|
||||||
|
|
||||||
const pathToServerBinary = resolve(
|
const pathToServerBinary = resolve(
|
||||||
join(await getTargetDir(), "debug/deltachat-rpc-server"),
|
join(await getTargetDir(), "debug/deltachat-rpc-server")
|
||||||
);
|
);
|
||||||
|
|
||||||
const server = spawn(pathToServerBinary, {
|
const server = spawn(pathToServerBinary, {
|
||||||
@@ -29,7 +29,7 @@ export async function startServer(): Promise<RpcServerHandle> {
|
|||||||
throw new Error(
|
throw new Error(
|
||||||
"Failed to start server executable " +
|
"Failed to start server executable " +
|
||||||
pathToServerBinary +
|
pathToServerBinary +
|
||||||
", make sure you built it first.",
|
", make sure you built it first."
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
let shouldClose = false;
|
let shouldClose = false;
|
||||||
@@ -83,7 +83,7 @@ function getTargetDir(): Promise<string> {
|
|||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-repl"
|
name = "deltachat-repl"
|
||||||
version = "2.50.0-dev"
|
version = "1.159.3"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
repository = "https://github.com/chatmail/core"
|
repository = "https://github.com/chatmail/core"
|
||||||
@@ -13,7 +13,7 @@ log = { workspace = true }
|
|||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
qr2term = "0.3.3"
|
qr2term = "0.3.3"
|
||||||
rusqlite = { workspace = true }
|
rusqlite = { workspace = true }
|
||||||
rustyline = "16"
|
rustyline = "15"
|
||||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ use std::str::FromStr;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{bail, ensure, Result};
|
use anyhow::{bail, ensure, Result};
|
||||||
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
|
use deltachat::chat::{
|
||||||
|
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
|
||||||
|
};
|
||||||
use deltachat::chatlist::*;
|
use deltachat::chatlist::*;
|
||||||
use deltachat::constants::*;
|
use deltachat::constants::*;
|
||||||
use deltachat::contact::*;
|
use deltachat::contact::*;
|
||||||
@@ -18,6 +20,7 @@ use deltachat::log::LogExt;
|
|||||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
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::peerstate::*;
|
||||||
use deltachat::qr::*;
|
use deltachat::qr::*;
|
||||||
use deltachat::qr_code_generator::create_qr_svg;
|
use deltachat::qr_code_generator::create_qr_svg;
|
||||||
use deltachat::reaction::send_reaction;
|
use deltachat::reaction::send_reaction;
|
||||||
@@ -32,6 +35,14 @@ use tokio::fs;
|
|||||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||||
async fn reset_tables(context: &Context, bits: i32) {
|
async fn reset_tables(context: &Context, bits: i32) {
|
||||||
println!("Resetting tables ({bits})...");
|
println!("Resetting tables ({bits})...");
|
||||||
|
if 0 != bits & 2 {
|
||||||
|
context
|
||||||
|
.sql()
|
||||||
|
.execute("DELETE FROM acpeerstates;", ())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
println!("(2) Peerstates reset.");
|
||||||
|
}
|
||||||
if 0 != bits & 4 {
|
if 0 != bits & 4 {
|
||||||
context
|
context
|
||||||
.sql()
|
.sql()
|
||||||
@@ -70,6 +81,11 @@ async fn reset_tables(context: &Context, bits: i32) {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
context.sql().config_cache().write().await.clear();
|
context.sql().config_cache().write().await.clear();
|
||||||
|
context
|
||||||
|
.sql()
|
||||||
|
.execute("DELETE FROM leftgrps;", ())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
println!("(8) Rest but server config reset.");
|
println!("(8) Rest but server config reset.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +96,7 @@ async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
|||||||
let data = read_file(context, filename).await?;
|
let data = read_file(context, filename).await?;
|
||||||
|
|
||||||
if let Err(err) = receive_imf(context, &data, false).await {
|
if let Err(err) = receive_imf(context, &data, false).await {
|
||||||
eprintln!("receive_imf errored: {err:?}");
|
println!("receive_imf errored: {err:?}");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -104,7 +120,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
|||||||
} else {
|
} else {
|
||||||
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
|
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
|
||||||
if rs.is_none() {
|
if rs.is_none() {
|
||||||
eprintln!("Import: No file or folder given.");
|
error!(context, "Import: No file or folder given.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
real_spec = rs.unwrap();
|
real_spec = rs.unwrap();
|
||||||
@@ -122,7 +138,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
|||||||
let name_f = entry.file_name();
|
let name_f = entry.file_name();
|
||||||
let name = name_f.to_string_lossy();
|
let name = name_f.to_string_lossy();
|
||||||
if name.ends_with(".eml") {
|
if name.ends_with(".eml") {
|
||||||
let path_plus_name = format!("{real_spec}/{name}");
|
let path_plus_name = format!("{}/{}", &real_spec, name);
|
||||||
println!("Import: {path_plus_name}");
|
println!("Import: {path_plus_name}");
|
||||||
if poke_eml_file(context, Path::new(&path_plus_name))
|
if poke_eml_file(context, Path::new(&path_plus_name))
|
||||||
.await
|
.await
|
||||||
@@ -133,11 +149,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Import: Cannot open directory {real_spec:?}.");
|
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("Import: {read_cnt} items read from {real_spec:?}.");
|
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
|
||||||
if read_cnt > 0 {
|
if read_cnt > 0 {
|
||||||
context.emit_msgs_changed_without_ids();
|
context.emit_msgs_changed_without_ids();
|
||||||
}
|
}
|
||||||
@@ -179,7 +195,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
|||||||
msg.get_id(),
|
msg.get_id(),
|
||||||
if msg.get_showpadlock() { "🔒" } else { "" },
|
if msg.get_showpadlock() { "🔒" } else { "" },
|
||||||
if msg.has_location() { "📍" } else { "" },
|
if msg.has_location() { "📍" } else { "" },
|
||||||
contact_name,
|
&contact_name,
|
||||||
contact_id,
|
contact_id,
|
||||||
msgtext,
|
msgtext,
|
||||||
if msg.has_html() { "[HAS-HTML]️" } else { "" },
|
if msg.has_html() { "[HAS-HTML]️" } else { "" },
|
||||||
@@ -203,7 +219,13 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
if msg.get_viewtype() == Viewtype::Webxdc {
|
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
||||||
|
format!(
|
||||||
|
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
||||||
|
msg.get_videochat_url().unwrap_or_default(),
|
||||||
|
msg.get_videochat_type().unwrap_or_default()
|
||||||
|
)
|
||||||
|
} else if msg.get_viewtype() == Viewtype::Webxdc {
|
||||||
match msg.get_webxdc_info(context).await {
|
match msg.get_webxdc_info(context).await {
|
||||||
Ok(info) => format!(
|
Ok(info) => format!(
|
||||||
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
|
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
|
||||||
@@ -221,7 +243,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
|||||||
},
|
},
|
||||||
statestr,
|
statestr,
|
||||||
downloadstate,
|
downloadstate,
|
||||||
temp2,
|
&temp2,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +277,7 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
|
|||||||
|
|
||||||
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
|
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
|
||||||
for contact_id in contacts {
|
for contact_id in contacts {
|
||||||
let line2 = "".to_string();
|
let mut line2 = "".to_string();
|
||||||
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();
|
||||||
@@ -274,6 +296,15 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
|
|||||||
verified_str,
|
verified_str,
|
||||||
if !addr.is_empty() { addr } else { "addr unset" }
|
if !addr.is_empty() { addr } else { "addr unset" }
|
||||||
);
|
);
|
||||||
|
let peerstate = Peerstate::from_addr(context, addr)
|
||||||
|
.await
|
||||||
|
.expect("peerstate error");
|
||||||
|
if peerstate.is_some() && *contact_id != ContactId::SELF {
|
||||||
|
line2 = format!(
|
||||||
|
", prefer-encrypt={}",
|
||||||
|
peerstate.as_ref().unwrap().prefer_encrypt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
println!("Contact#{}: {}{}", *contact_id, line, line2);
|
println!("Contact#{}: {}{}", *contact_id, line, line2);
|
||||||
}
|
}
|
||||||
@@ -302,13 +333,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
// TODO: reuse commands definition in main.rs.
|
// TODO: reuse commands definition in main.rs.
|
||||||
"imex" => println!(
|
"imex" => println!(
|
||||||
"====================Import/Export commands==\n\
|
"====================Import/Export commands==\n\
|
||||||
|
initiate-key-transfer\n\
|
||||||
|
get-setupcodebegin <msg-id>\n\
|
||||||
|
continue-key-transfer <msg-id> <setup-code>\n\
|
||||||
has-backup\n\
|
has-backup\n\
|
||||||
export-backup\n\
|
export-backup\n\
|
||||||
import-backup <backup-file>\n\
|
import-backup <backup-file>\n\
|
||||||
send-backup\n\
|
send-backup\n\
|
||||||
receive-backup <qr>\n\
|
receive-backup <qr>\n\
|
||||||
export-keys\n\
|
export-keys\n\
|
||||||
import-keys <key-file>\n\
|
import-keys\n\
|
||||||
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
||||||
reset <flags>\n\
|
reset <flags>\n\
|
||||||
stop\n\
|
stop\n\
|
||||||
@@ -317,6 +351,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
_ => println!(
|
_ => println!(
|
||||||
"==========================Database commands==\n\
|
"==========================Database commands==\n\
|
||||||
info\n\
|
info\n\
|
||||||
|
open <file to open or create>\n\
|
||||||
|
close\n\
|
||||||
set <configuration-key> [<value>]\n\
|
set <configuration-key> [<value>]\n\
|
||||||
get <configuration-key>\n\
|
get <configuration-key>\n\
|
||||||
oauth2\n\
|
oauth2\n\
|
||||||
@@ -331,30 +367,28 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
==============================Chat commands==\n\
|
==============================Chat commands==\n\
|
||||||
listchats [<query>]\n\
|
listchats [<query>]\n\
|
||||||
listarchived\n\
|
listarchived\n\
|
||||||
start-realtime <msg-id>\n\
|
|
||||||
send-realtime <msg-id> <data>\n\
|
|
||||||
chat [<chat-id>|0]\n\
|
chat [<chat-id>|0]\n\
|
||||||
createchat <contact-id>\n\
|
createchat <contact-id>\n\
|
||||||
creategroup <name>\n\
|
creategroup <name>\n\
|
||||||
createbroadcast <name>\n\
|
createbroadcast\n\
|
||||||
|
createprotected <name>\n\
|
||||||
addmember <contact-id>\n\
|
addmember <contact-id>\n\
|
||||||
removemember <contact-id>\n\
|
removemember <contact-id>\n\
|
||||||
groupname <name>\n\
|
groupname <name>\n\
|
||||||
groupdescription <description>\n\
|
groupimage [<file>]\n\
|
||||||
groupimage <image>\n\
|
|
||||||
chatinfo\n\
|
chatinfo\n\
|
||||||
sendlocations <seconds>\n\
|
sendlocations <seconds>\n\
|
||||||
setlocation <lat> <lng>\n\
|
setlocation <lat> <lng>\n\
|
||||||
|
dellocations\n\
|
||||||
getlocations [<contact-id>]\n\
|
getlocations [<contact-id>]\n\
|
||||||
send <text>\n\
|
send <text>\n\
|
||||||
send-sync <text>\n\
|
|
||||||
sendempty\n\
|
|
||||||
sendimage <file> [<text>]\n\
|
sendimage <file> [<text>]\n\
|
||||||
sendsticker <file> [<text>]\n\
|
sendsticker <file> [<text>]\n\
|
||||||
sendfile <file> [<text>]\n\
|
sendfile <file> [<text>]\n\
|
||||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||||
sendsyncmsg\n\
|
sendsyncmsg\n\
|
||||||
sendupdate <msg-id> <json status update>\n\
|
sendupdate <msg-id> <json status update>\n\
|
||||||
|
videochat\n\
|
||||||
draft [<text>]\n\
|
draft [<text>]\n\
|
||||||
devicemsg <text>\n\
|
devicemsg <text>\n\
|
||||||
listmedia\n\
|
listmedia\n\
|
||||||
@@ -366,7 +400,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
unmute <chat-id>\n\
|
unmute <chat-id>\n\
|
||||||
delchat <chat-id>\n\
|
delchat <chat-id>\n\
|
||||||
accept <chat-id>\n\
|
accept <chat-id>\n\
|
||||||
blockchat <chat-id>\n\
|
decline <chat-id>\n\
|
||||||
===========================Message commands==\n\
|
===========================Message commands==\n\
|
||||||
listmsgs <query>\n\
|
listmsgs <query>\n\
|
||||||
msginfo <msg-id>\n\
|
msginfo <msg-id>\n\
|
||||||
@@ -380,14 +414,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
react <msg-id> [<reaction>]\n\
|
react <msg-id> [<reaction>]\n\
|
||||||
===========================Contact commands==\n\
|
===========================Contact commands==\n\
|
||||||
listcontacts [<query>]\n\
|
listcontacts [<query>]\n\
|
||||||
|
listverified [<query>]\n\
|
||||||
addcontact [<name>] <addr>\n\
|
addcontact [<name>] <addr>\n\
|
||||||
contactinfo <contact-id>\n\
|
contactinfo <contact-id>\n\
|
||||||
delcontact <contact-id>\n\
|
delcontact <contact-id>\n\
|
||||||
|
cleanupcontacts\n\
|
||||||
block <contact-id>\n\
|
block <contact-id>\n\
|
||||||
unblock <contact-id>\n\
|
unblock <contact-id>\n\
|
||||||
listblocked\n\
|
listblocked\n\
|
||||||
import-vcard <file>\n\
|
|
||||||
make-vcard <file> <contact-id> [contact-id ...]\n\
|
|
||||||
======================================Misc.==\n\
|
======================================Misc.==\n\
|
||||||
getqr [<chat-id>]\n\
|
getqr [<chat-id>]\n\
|
||||||
getqrsvg [<chat-id>]\n\
|
getqrsvg [<chat-id>]\n\
|
||||||
@@ -404,6 +438,34 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
============================================="
|
============================================="
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
"initiate-key-transfer" => match initiate_key_transfer(&context).await {
|
||||||
|
Ok(setup_code) => {
|
||||||
|
println!("Setup code for the transferred setup message: {setup_code}",)
|
||||||
|
}
|
||||||
|
Err(err) => bail!("Failed to generate setup code: {}", err),
|
||||||
|
},
|
||||||
|
"get-setupcodebegin" => {
|
||||||
|
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||||
|
let msg_id: MsgId = MsgId::new(arg1.parse()?);
|
||||||
|
let msg = Message::load_from_db(&context, msg_id).await?;
|
||||||
|
if msg.is_setupmessage() {
|
||||||
|
let setupcodebegin = msg.get_setupcodebegin(&context).await;
|
||||||
|
println!(
|
||||||
|
"The setup code for setup message {} starts with: {}",
|
||||||
|
msg_id,
|
||||||
|
setupcodebegin.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
bail!("{} is no setup message.", msg_id,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"continue-key-transfer" => {
|
||||||
|
ensure!(
|
||||||
|
!arg1.is_empty() && !arg2.is_empty(),
|
||||||
|
"Arguments <msg-id> <setup-code> expected"
|
||||||
|
);
|
||||||
|
continue_key_transfer(&context, MsgId::new(arg1.parse()?), arg2).await?;
|
||||||
|
}
|
||||||
"has-backup" => {
|
"has-backup" => {
|
||||||
has_backup(&context, blobdir).await?;
|
has_backup(&context, blobdir).await?;
|
||||||
}
|
}
|
||||||
@@ -431,7 +493,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
"send-backup" => {
|
"send-backup" => {
|
||||||
let provider = BackupProvider::prepare(&context).await?;
|
let provider = BackupProvider::prepare(&context).await?;
|
||||||
let qr = format_backup(&provider.qr())?;
|
let qr = format_backup(&provider.qr())?;
|
||||||
println!("QR code: {qr}");
|
println!("QR code: {}", qr);
|
||||||
qr2term::print_qr(qr.as_str())?;
|
qr2term::print_qr(qr.as_str())?;
|
||||||
provider.await?;
|
provider.await?;
|
||||||
}
|
}
|
||||||
@@ -446,17 +508,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
println!("Exported to {}.", dir.to_string_lossy());
|
println!("Exported to {}.", dir.to_string_lossy());
|
||||||
}
|
}
|
||||||
"import-keys" => {
|
"import-keys" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <key-file> missing.");
|
|
||||||
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
|
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
|
||||||
}
|
}
|
||||||
"poke" => {
|
"poke" => {
|
||||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||||
}
|
}
|
||||||
"reset" => {
|
"reset" => {
|
||||||
ensure!(
|
ensure!(!arg1.is_empty(), "Argument <bits> missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config");
|
||||||
!arg1.is_empty(),
|
|
||||||
"Argument <bits> missing: 4=private keys, 8=rest but server config"
|
|
||||||
);
|
|
||||||
let bits: i32 = arg1.parse()?;
|
let bits: i32 = arg1.parse()?;
|
||||||
ensure!(bits < 16, "<bits> must be lower than 16.");
|
ensure!(bits < 16, "<bits> must be lower than 16.");
|
||||||
reset_tables(&context, bits).await;
|
reset_tables(&context, bits).await;
|
||||||
@@ -489,7 +547,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
println!("Report written to: {file:#?}");
|
println!("Report written to: {file:#?}");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
bail!("Failed to get connectivity html: {err}");
|
bail!("Failed to get connectivity html: {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,7 +582,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
for i in (0..cnt).rev() {
|
for i in (0..cnt).rev() {
|
||||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
|
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
|
||||||
println!(
|
println!(
|
||||||
"{}#{}: {} [{} fresh] {}{}{}",
|
"{}#{}: {} [{} fresh] {}{}{}{}",
|
||||||
chat_prefix(&chat),
|
chat_prefix(&chat),
|
||||||
chat.get_id(),
|
chat.get_id(),
|
||||||
chat.get_name(),
|
chat.get_name(),
|
||||||
@@ -535,6 +593,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
ChatVisibility::Archived => "📦",
|
ChatVisibility::Archived => "📦",
|
||||||
ChatVisibility::Pinned => "📌",
|
ChatVisibility::Pinned => "📌",
|
||||||
},
|
},
|
||||||
|
if chat.is_protected() { "🛡️" } else { "" },
|
||||||
if chat.is_contact_request() {
|
if chat.is_contact_request() {
|
||||||
"🆕"
|
"🆕"
|
||||||
} else {
|
} else {
|
||||||
@@ -561,7 +620,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
.map_or_else(String::new, |prefix| format!("{prefix}: ")),
|
.map_or_else(String::new, |prefix| format!("{prefix}: ")),
|
||||||
summary.text,
|
summary.text,
|
||||||
statestr,
|
statestr,
|
||||||
timestr,
|
×tr,
|
||||||
if chat.is_sending_locations() {
|
if chat.is_sending_locations() {
|
||||||
"📍"
|
"📍"
|
||||||
} else {
|
} else {
|
||||||
@@ -573,11 +632,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if location::is_sending(&context).await? {
|
if location::is_sending_locations_to_chat(&context, None).await? {
|
||||||
println!("Location streaming enabled.");
|
println!("Location streaming enabled.");
|
||||||
}
|
}
|
||||||
println!("{cnt} chats");
|
println!("{cnt} chats");
|
||||||
eprintln!("{time_needed:?} to create this list");
|
println!("{time_needed:?} to create this list");
|
||||||
}
|
}
|
||||||
"start-realtime" => {
|
"start-realtime" => {
|
||||||
if arg1.is_empty() {
|
if arg1.is_empty() {
|
||||||
@@ -622,6 +681,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
&context,
|
&context,
|
||||||
sel_chat.get_id(),
|
sel_chat.get_id(),
|
||||||
chat::MessageListOptions {
|
chat::MessageListOptions {
|
||||||
|
info_only: false,
|
||||||
add_daymarker: true,
|
add_daymarker: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -648,7 +708,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
format!("{} member(s)", members.len())
|
format!("{} member(s)", members.len())
|
||||||
};
|
};
|
||||||
println!(
|
println!(
|
||||||
"{}#{}: {} [{}]{}{}{}",
|
"{}#{}: {} [{}]{}{}{} {}",
|
||||||
chat_prefix(sel_chat),
|
chat_prefix(sel_chat),
|
||||||
sel_chat.get_id(),
|
sel_chat.get_id(),
|
||||||
sel_chat.get_name(),
|
sel_chat.get_name(),
|
||||||
@@ -666,6 +726,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
},
|
},
|
||||||
_ => "".to_string(),
|
_ => "".to_string(),
|
||||||
},
|
},
|
||||||
|
if sel_chat.is_protected() {
|
||||||
|
"🛡️"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
);
|
);
|
||||||
log_msglist(&context, &msglist).await?;
|
log_msglist(&context, &msglist).await?;
|
||||||
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
|
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
|
||||||
@@ -681,7 +746,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
|
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
|
||||||
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
|
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
|
||||||
|
|
||||||
eprintln!(
|
println!(
|
||||||
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
|
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -694,16 +759,23 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
}
|
}
|
||||||
"creategroup" => {
|
"creategroup" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||||
let chat_id = chat::create_group(&context, arg1).await?;
|
let chat_id =
|
||||||
|
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
|
||||||
|
|
||||||
println!("Group#{chat_id} created successfully.");
|
println!("Group#{chat_id} created successfully.");
|
||||||
}
|
}
|
||||||
"createbroadcast" => {
|
"createbroadcast" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
let chat_id = chat::create_broadcast_list(&context).await?;
|
||||||
let chat_id = chat::create_broadcast(&context, arg1.to_string()).await?;
|
|
||||||
|
|
||||||
println!("Broadcast#{chat_id} created successfully.");
|
println!("Broadcast#{chat_id} created successfully.");
|
||||||
}
|
}
|
||||||
|
"createprotected" => {
|
||||||
|
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||||
|
let chat_id =
|
||||||
|
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
|
||||||
|
|
||||||
|
println!("Group#{chat_id} created and protected successfully.");
|
||||||
|
}
|
||||||
"addmember" => {
|
"addmember" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected");
|
ensure!(sel_chat.is_some(), "No chat selected");
|
||||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||||
@@ -738,13 +810,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
println!("Chat name set");
|
println!("Chat name set");
|
||||||
}
|
}
|
||||||
"groupdescription" => {
|
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
|
||||||
ensure!(!arg1.is_empty(), "Argument <description> missing.");
|
|
||||||
chat::set_chat_description(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?;
|
|
||||||
|
|
||||||
println!("Chat description set");
|
|
||||||
}
|
|
||||||
"groupimage" => {
|
"groupimage" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
ensure!(!arg1.is_empty(), "Argument <image> missing.");
|
ensure!(!arg1.is_empty(), "Argument <image> missing.");
|
||||||
@@ -780,7 +845,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Location streaming: {}",
|
"Location streaming: {}",
|
||||||
location::is_sending_to_chat(&context, sel_chat.as_ref().unwrap().get_id()).await?,
|
location::is_sending_locations_to_chat(
|
||||||
|
&context,
|
||||||
|
Some(sel_chat.as_ref().unwrap().get_id())
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
"getlocations" => {
|
"getlocations" => {
|
||||||
@@ -820,7 +889,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
ensure!(!arg1.is_empty(), "No timeout given.");
|
ensure!(!arg1.is_empty(), "No timeout given.");
|
||||||
|
|
||||||
let seconds = arg1.parse()?;
|
let seconds = arg1.parse()?;
|
||||||
location::send_to_chat(&context, sel_chat.as_ref().unwrap().get_id(), seconds).await?;
|
location::send_locations_to_chat(
|
||||||
|
&context,
|
||||||
|
sel_chat.as_ref().unwrap().get_id(),
|
||||||
|
seconds,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
println!(
|
println!(
|
||||||
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
|
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
|
||||||
sel_chat.as_ref().unwrap().get_id(),
|
sel_chat.as_ref().unwrap().get_id(),
|
||||||
@@ -842,6 +916,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
println!("Success, streaming can be stopped.");
|
println!("Success, streaming can be stopped.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"dellocations" => {
|
||||||
|
location::delete_all(&context).await?;
|
||||||
|
}
|
||||||
"send" => {
|
"send" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
ensure!(!arg1.is_empty(), "No message text given.");
|
ensure!(!arg1.is_empty(), "No message text given.");
|
||||||
@@ -850,23 +927,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?;
|
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?;
|
||||||
}
|
}
|
||||||
"send-sync" => {
|
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
|
||||||
ensure!(!arg1.is_empty(), "No message text given.");
|
|
||||||
|
|
||||||
// Send message over a dedicated SMTP connection
|
|
||||||
// and measure time.
|
|
||||||
//
|
|
||||||
// This can be used to benchmark SMTP connection establishment.
|
|
||||||
let time_start = std::time::Instant::now();
|
|
||||||
|
|
||||||
let msg = format!("{arg1} {arg2}");
|
|
||||||
let mut msg = Message::new_text(msg);
|
|
||||||
chat::send_msg_sync(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
|
||||||
|
|
||||||
let time_needed = time_start.elapsed();
|
|
||||||
println!("Sent message in {time_needed:?}.");
|
|
||||||
}
|
|
||||||
"sendempty" => {
|
"sendempty" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
|
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
|
||||||
@@ -914,6 +974,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
let msg_id = MsgId::new(arg1.parse()?);
|
let msg_id = MsgId::new(arg1.parse()?);
|
||||||
context.send_webxdc_status_update(msg_id, arg2).await?;
|
context.send_webxdc_status_update(msg_id, arg2).await?;
|
||||||
}
|
}
|
||||||
|
"videochat" => {
|
||||||
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
|
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||||
|
}
|
||||||
"listmsgs" => {
|
"listmsgs" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
||||||
|
|
||||||
@@ -935,7 +999,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
},
|
},
|
||||||
query,
|
query,
|
||||||
);
|
);
|
||||||
eprintln!("{time_needed:?} to create this list");
|
println!("{time_needed:?} to create this list");
|
||||||
}
|
}
|
||||||
"draft" => {
|
"draft" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
@@ -1098,13 +1162,19 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
let reaction = arg2;
|
let reaction = arg2;
|
||||||
send_reaction(&context, msg_id, reaction).await?;
|
send_reaction(&context, msg_id, reaction).await?;
|
||||||
}
|
}
|
||||||
"listcontacts" | "contacts" => {
|
"listcontacts" | "contacts" | "listverified" => {
|
||||||
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
|
let contacts = Contact::get_all(
|
||||||
|
&context,
|
||||||
|
if arg0 == "listverified" {
|
||||||
|
DC_GCL_VERIFIED_ONLY | DC_GCL_ADD_SELF
|
||||||
|
} else {
|
||||||
|
DC_GCL_ADD_SELF
|
||||||
|
},
|
||||||
|
Some(arg1),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
log_contactlist(&context, &contacts).await?;
|
log_contactlist(&context, &contacts).await?;
|
||||||
println!("{} key contacts.", contacts.len());
|
println!("{} contacts.", contacts.len());
|
||||||
let addrcontacts = Contact::get_all(&context, DC_GCL_ADDRESS, Some(arg1)).await?;
|
|
||||||
log_contactlist(&context, &addrcontacts).await?;
|
|
||||||
println!("{} address contacts.", addrcontacts.len());
|
|
||||||
}
|
}
|
||||||
"addcontact" => {
|
"addcontact" => {
|
||||||
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
|
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
|
||||||
@@ -1168,24 +1238,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
log_contactlist(&context, &contacts).await?;
|
log_contactlist(&context, &contacts).await?;
|
||||||
println!("{} blocked contacts.", contacts.len());
|
println!("{} blocked contacts.", contacts.len());
|
||||||
}
|
}
|
||||||
"import-vcard" => {
|
|
||||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
|
||||||
let vcard_content = fs::read_to_string(&arg1.to_string()).await?;
|
|
||||||
let contacts = import_vcard(&context, &vcard_content).await?;
|
|
||||||
println!("vCard contacts imported:");
|
|
||||||
log_contactlist(&context, &contacts).await?;
|
|
||||||
}
|
|
||||||
"make-vcard" => {
|
|
||||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
|
||||||
ensure!(!arg2.is_empty(), "Argument <contact-id> missing.");
|
|
||||||
let mut contact_ids = vec![];
|
|
||||||
for x in arg2.split_whitespace() {
|
|
||||||
contact_ids.push(ContactId::new(x.parse()?))
|
|
||||||
}
|
|
||||||
let vcard_content = make_vcard(&context, &contact_ids).await?;
|
|
||||||
fs::write(&arg1.to_string(), vcard_content).await?;
|
|
||||||
println!("vCard written to: {arg1}");
|
|
||||||
}
|
|
||||||
"checkqr" => {
|
"checkqr" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||||
let qr = check_qr(&context, arg1).await?;
|
let qr = check_qr(&context, arg1).await?;
|
||||||
@@ -1194,8 +1246,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
"setqr" => {
|
"setqr" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||||
match set_config_from_qr(&context, arg1).await {
|
match set_config_from_qr(&context, arg1).await {
|
||||||
Ok(()) => eprintln!("Config set from the QR code."),
|
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
|
||||||
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
|
Err(err) => println!("Cannot set config from QR code: {err:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"createqrsvg" => {
|
"createqrsvg" => {
|
||||||
@@ -1207,7 +1259,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
}
|
}
|
||||||
"providerinfo" => {
|
"providerinfo" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
||||||
match provider::get_provider_info(arg1) {
|
let proxy_enabled = context
|
||||||
|
.get_config_bool(config::Config::ProxyEnabled)
|
||||||
|
.await?;
|
||||||
|
match provider::get_provider_info(&context, arg1, proxy_enabled).await {
|
||||||
Some(info) => {
|
Some(info) => {
|
||||||
println!("Information for provider belonging to {arg1}:");
|
println!("Information for provider belonging to {arg1}:");
|
||||||
println!("status: {}", info.status as u32);
|
println!("status: {}", info.status as u32);
|
||||||
@@ -1243,7 +1298,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
"" => (),
|
"" => (),
|
||||||
_ => bail!("Unknown command: \"{arg0}\" type ? for help."),
|
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
//! Usage: cargo run --example repl --release -- <databasefile>
|
//! Usage: cargo run --example repl --release -- <databasefile>
|
||||||
//! All further options can be set using the set-command (type ? for help).
|
//! All further options can be set using the set-command (type ? for help).
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
extern crate deltachat;
|
extern crate deltachat;
|
||||||
|
|
||||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||||
@@ -40,25 +41,25 @@ fn receive_event(event: EventType) {
|
|||||||
match event {
|
match event {
|
||||||
EventType::Info(msg) => {
|
EventType::Info(msg) => {
|
||||||
/* do not show the event as this would fill the screen */
|
/* do not show the event as this would fill the screen */
|
||||||
info!("{msg}");
|
info!("{}", msg);
|
||||||
}
|
}
|
||||||
EventType::SmtpConnected(msg) => {
|
EventType::SmtpConnected(msg) => {
|
||||||
info!("[SMTP_CONNECTED] {msg}");
|
info!("[SMTP_CONNECTED] {}", msg);
|
||||||
}
|
}
|
||||||
EventType::ImapConnected(msg) => {
|
EventType::ImapConnected(msg) => {
|
||||||
info!("[IMAP_CONNECTED] {msg}");
|
info!("[IMAP_CONNECTED] {}", msg);
|
||||||
}
|
}
|
||||||
EventType::SmtpMessageSent(msg) => {
|
EventType::SmtpMessageSent(msg) => {
|
||||||
info!("[SMTP_MESSAGE_SENT] {msg}");
|
info!("[SMTP_MESSAGE_SENT] {}", msg);
|
||||||
}
|
}
|
||||||
EventType::Warning(msg) => {
|
EventType::Warning(msg) => {
|
||||||
warn!("{msg}");
|
warn!("{}", msg);
|
||||||
}
|
}
|
||||||
EventType::Error(msg) => {
|
EventType::Error(msg) => {
|
||||||
error!("{msg}");
|
error!("{}", msg);
|
||||||
}
|
}
|
||||||
EventType::ErrorSelfNotInGroup(msg) => {
|
EventType::ErrorSelfNotInGroup(msg) => {
|
||||||
error!("[SELF_NOT_IN_GROUP] {msg}");
|
error!("[SELF_NOT_IN_GROUP] {}", msg);
|
||||||
}
|
}
|
||||||
EventType::MsgsChanged { chat_id, msg_id } => {
|
EventType::MsgsChanged { chat_id, msg_id } => {
|
||||||
info!(
|
info!(
|
||||||
@@ -123,7 +124,7 @@ fn receive_event(event: EventType) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
info!("Received {event:?}");
|
info!("Received {:?}", event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +150,10 @@ impl Completer for DcHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const IMEX_COMMANDS: [&str; 10] = [
|
const IMEX_COMMANDS: [&str; 13] = [
|
||||||
|
"initiate-key-transfer",
|
||||||
|
"get-setupcodebegin",
|
||||||
|
"continue-key-transfer",
|
||||||
"has-backup",
|
"has-backup",
|
||||||
"export-backup",
|
"export-backup",
|
||||||
"import-backup",
|
"import-backup",
|
||||||
@@ -176,11 +180,9 @@ const DB_COMMANDS: [&str; 11] = [
|
|||||||
"housekeeping",
|
"housekeeping",
|
||||||
];
|
];
|
||||||
|
|
||||||
const CHAT_COMMANDS: [&str; 39] = [
|
const CHAT_COMMANDS: [&str; 36] = [
|
||||||
"listchats",
|
"listchats",
|
||||||
"listarchived",
|
"listarchived",
|
||||||
"start-realtime",
|
|
||||||
"send-realtime",
|
|
||||||
"chat",
|
"chat",
|
||||||
"createchat",
|
"createchat",
|
||||||
"creategroup",
|
"creategroup",
|
||||||
@@ -189,23 +191,20 @@ const CHAT_COMMANDS: [&str; 39] = [
|
|||||||
"addmember",
|
"addmember",
|
||||||
"removemember",
|
"removemember",
|
||||||
"groupname",
|
"groupname",
|
||||||
"groupdescription",
|
|
||||||
"groupimage",
|
"groupimage",
|
||||||
"chatinfo",
|
"chatinfo",
|
||||||
"sendlocations",
|
"sendlocations",
|
||||||
"setlocation",
|
"setlocation",
|
||||||
|
"dellocations",
|
||||||
"getlocations",
|
"getlocations",
|
||||||
"send",
|
"send",
|
||||||
"send-sync",
|
|
||||||
"sendempty",
|
|
||||||
"sendimage",
|
"sendimage",
|
||||||
"sendsticker",
|
|
||||||
"sendfile",
|
"sendfile",
|
||||||
"sendhtml",
|
"sendhtml",
|
||||||
"sendsyncmsg",
|
"sendsyncmsg",
|
||||||
"sendupdate",
|
"sendupdate",
|
||||||
|
"videochat",
|
||||||
"draft",
|
"draft",
|
||||||
"devicemsg",
|
|
||||||
"listmedia",
|
"listmedia",
|
||||||
"archive",
|
"archive",
|
||||||
"unarchive",
|
"unarchive",
|
||||||
@@ -213,48 +212,47 @@ const CHAT_COMMANDS: [&str; 39] = [
|
|||||||
"unpin",
|
"unpin",
|
||||||
"mute",
|
"mute",
|
||||||
"unmute",
|
"unmute",
|
||||||
|
"protect",
|
||||||
|
"unprotect",
|
||||||
"delchat",
|
"delchat",
|
||||||
"accept",
|
"accept",
|
||||||
"blockchat",
|
"blockchat",
|
||||||
];
|
];
|
||||||
const MESSAGE_COMMANDS: [&str; 10] = [
|
const MESSAGE_COMMANDS: [&str; 9] = [
|
||||||
"listmsgs",
|
"listmsgs",
|
||||||
"msginfo",
|
"msginfo",
|
||||||
"download",
|
|
||||||
"html",
|
|
||||||
"listfresh",
|
"listfresh",
|
||||||
"forward",
|
"forward",
|
||||||
"resend",
|
"resend",
|
||||||
"markseen",
|
"markseen",
|
||||||
"delmsg",
|
"delmsg",
|
||||||
|
"download",
|
||||||
"react",
|
"react",
|
||||||
];
|
];
|
||||||
const CONTACT_COMMANDS: [&str; 9] = [
|
const CONTACT_COMMANDS: [&str; 9] = [
|
||||||
"listcontacts",
|
"listcontacts",
|
||||||
|
"listverified",
|
||||||
"addcontact",
|
"addcontact",
|
||||||
"contactinfo",
|
"contactinfo",
|
||||||
"delcontact",
|
"delcontact",
|
||||||
|
"cleanupcontacts",
|
||||||
"block",
|
"block",
|
||||||
"unblock",
|
"unblock",
|
||||||
"listblocked",
|
"listblocked",
|
||||||
"import-vcard",
|
|
||||||
"make-vcard",
|
|
||||||
];
|
];
|
||||||
const MISC_COMMANDS: [&str; 14] = [
|
const MISC_COMMANDS: [&str; 12] = [
|
||||||
"getqr",
|
"getqr",
|
||||||
"getqrsvg",
|
"getqrsvg",
|
||||||
"getbadqr",
|
"getbadqr",
|
||||||
"checkqr",
|
"checkqr",
|
||||||
"joinqr",
|
"joinqr",
|
||||||
"setqr",
|
|
||||||
"createqrsvg",
|
"createqrsvg",
|
||||||
"providerinfo",
|
|
||||||
"fileinfo",
|
"fileinfo",
|
||||||
"estimatedeletion",
|
|
||||||
"clear",
|
"clear",
|
||||||
"exit",
|
"exit",
|
||||||
"quit",
|
"quit",
|
||||||
"help",
|
"help",
|
||||||
|
"estimatedeletion",
|
||||||
];
|
];
|
||||||
|
|
||||||
impl Hinter for DcHelper {
|
impl Hinter for DcHelper {
|
||||||
@@ -310,7 +308,7 @@ impl Validator for DcHelper {}
|
|||||||
|
|
||||||
async fn start(args: Vec<String>) -> Result<(), Error> {
|
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
eprintln!("Error: Bad arguments, expected [db-name].");
|
println!("Error: Bad arguments, expected [db-name].");
|
||||||
bail!("No db-name specified");
|
bail!("No db-name specified");
|
||||||
}
|
}
|
||||||
let context = ContextBuilder::new(args[1].clone().into())
|
let context = ContextBuilder::new(args[1].clone().into())
|
||||||
@@ -365,7 +363,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error: {err:#}");
|
println!("Error: {err:#}");
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,7 +378,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error: {err:#}");
|
println!("Error: {err:#}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,12 +425,12 @@ async fn handle_cmd(
|
|||||||
}
|
}
|
||||||
"oauth2" => {
|
"oauth2" => {
|
||||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
||||||
if let Some(oauth2_url) =
|
let oauth2_url =
|
||||||
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?
|
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
|
||||||
{
|
if oauth2_url.is_none() {
|
||||||
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
|
println!("OAuth2 not available for {}.", &addr);
|
||||||
} else {
|
} else {
|
||||||
println!("OAuth2 not available for {addr}.");
|
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{}", oauth2_url.unwrap());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("oauth2: set addr first.");
|
println!("oauth2: set addr first.");
|
||||||
@@ -464,7 +462,7 @@ async fn handle_cmd(
|
|||||||
println!("QR code svg written to: {file:#?}");
|
println!("QR code svg written to: {file:#?}");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
bail!("Failed to get QR code svg: {err}");
|
bail!("Failed to get QR code svg: {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
RPC client connects to standalone Delta Chat RPC server `deltachat-rpc-server`
|
RPC client connects to standalone Delta Chat RPC server `deltachat-rpc-server`
|
||||||
and provides asynchronous interface to it.
|
and provides asynchronous interface to it.
|
||||||
`rpc.start()` performs a health-check RPC call to verify the server
|
|
||||||
started successfully and will raise an error if startup fails
|
|
||||||
(e.g. if the accounts directory could not be used).
|
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
@@ -33,15 +30,6 @@ $ pip install .
|
|||||||
|
|
||||||
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
||||||
|
|
||||||
|
|
||||||
## Activating current checkout of deltachat-rpc-client and -server for development
|
|
||||||
|
|
||||||
Go to root repository directory and run:
|
|
||||||
```
|
|
||||||
$ scripts/make-rpc-testenv.sh
|
|
||||||
$ source venv/bin/activate
|
|
||||||
```
|
|
||||||
|
|
||||||
## Using in REPL
|
## Using in REPL
|
||||||
|
|
||||||
Setup a development environment:
|
Setup a development environment:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ def main():
|
|||||||
with Rpc() as rpc:
|
with Rpc() as rpc:
|
||||||
deltachat = DeltaChat(rpc)
|
deltachat = DeltaChat(rpc)
|
||||||
system_info = deltachat.get_system_info()
|
system_info = deltachat.get_system_info()
|
||||||
logging.info(f"Running deltachat core {system_info['deltachat_core_version']}")
|
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
|
||||||
|
|
||||||
accounts = deltachat.get_all_accounts()
|
accounts = deltachat.get_all_accounts()
|
||||||
account = accounts[0] if accounts else deltachat.add_account()
|
account = accounts[0] if accounts else deltachat.add_account()
|
||||||
@@ -21,30 +21,36 @@ def main():
|
|||||||
account.set_config("bot", "1")
|
account.set_config("bot", "1")
|
||||||
if not account.is_configured():
|
if not account.is_configured():
|
||||||
logging.info("Account is not configured, configuring")
|
logging.info("Account is not configured, configuring")
|
||||||
account.add_or_update_transport({"addr": sys.argv[1], "password": sys.argv[2]})
|
account.set_config("addr", sys.argv[1])
|
||||||
|
account.set_config("mail_pw", sys.argv[2])
|
||||||
|
account.configure()
|
||||||
logging.info("Configured")
|
logging.info("Configured")
|
||||||
else:
|
else:
|
||||||
logging.info("Account is already configured")
|
logging.info("Account is already configured")
|
||||||
deltachat.start_io()
|
deltachat.start_io()
|
||||||
|
|
||||||
qr = account.get_qr_code()
|
def process_messages():
|
||||||
logging.info(f"Invite link: {qr}")
|
for message in account.get_next_messages():
|
||||||
while True:
|
|
||||||
event = account.wait_for_event()
|
|
||||||
if event.kind == EventType.INFO:
|
|
||||||
logging.info(event["msg"])
|
|
||||||
elif event.kind == EventType.WARNING:
|
|
||||||
logging.warning(event["msg"])
|
|
||||||
elif event.kind == EventType.ERROR:
|
|
||||||
logging.error(event["msg"])
|
|
||||||
elif event.kind == EventType.INCOMING_MSG:
|
|
||||||
logging.info("Got an incoming message")
|
|
||||||
message = account.get_message_by_id(event.msg_id)
|
|
||||||
snapshot = message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
|
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
|
||||||
snapshot.chat.send_text(snapshot.text)
|
snapshot.chat.send_text(snapshot.text)
|
||||||
snapshot.message.mark_seen()
|
snapshot.message.mark_seen()
|
||||||
|
|
||||||
|
# Process old messages.
|
||||||
|
process_messages()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
event = account.wait_for_event()
|
||||||
|
if event["kind"] == EventType.INFO:
|
||||||
|
logging.info("%s", event["msg"])
|
||||||
|
elif event["kind"] == EventType.WARNING:
|
||||||
|
logging.warning("%s", event["msg"])
|
||||||
|
elif event["kind"] == EventType.ERROR:
|
||||||
|
logging.error("%s", event["msg"])
|
||||||
|
elif event["kind"] == EventType.INCOMING_MSG:
|
||||||
|
logging.info("Got an incoming message")
|
||||||
|
process_messages()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=77"]
|
requires = ["setuptools>=45"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "deltachat-rpc-client"
|
name = "deltachat-rpc-client"
|
||||||
version = "2.50.0-dev"
|
version = "1.159.3"
|
||||||
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 = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||||
"Operating System :: POSIX :: Linux",
|
"Operating System :: POSIX :: Linux",
|
||||||
"Operating System :: MacOS :: MacOS X",
|
"Operating System :: MacOS :: MacOS X",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
"Programming Language :: Python :: 3.13",
|
"Programming Language :: Python :: 3.13",
|
||||||
"Programming Language :: Python :: 3.14",
|
|
||||||
"Topic :: Communications :: Chat",
|
"Topic :: Communications :: Chat",
|
||||||
"Topic :: Communications :: Email"
|
"Topic :: Communications :: Email"
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.8"
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
deltachat_rpc_client = [
|
deltachat_rpc_client = [
|
||||||
@@ -65,9 +66,6 @@ lint.select = [
|
|||||||
|
|
||||||
"RUF006" # asyncio-dangling-task
|
"RUF006" # asyncio-dangling-task
|
||||||
]
|
]
|
||||||
lint.ignore = [
|
|
||||||
"PLC0415" # `import` should be at the top-level of a file
|
|
||||||
]
|
|
||||||
line-length = 120
|
line-length = 120
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Delta Chat JSON-RPC high-level API."""
|
"""Delta Chat JSON-RPC high-level API"""
|
||||||
|
|
||||||
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
||||||
from .account import Account
|
from .account import Account
|
||||||
@@ -8,7 +8,7 @@ from .const import EventType, SpecialContactId
|
|||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
from .deltachat import DeltaChat
|
from .deltachat import DeltaChat
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from .rpc import JsonRpcError, Rpc
|
from .rpc import Rpc
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Account",
|
"Account",
|
||||||
@@ -19,7 +19,6 @@ __all__ = [
|
|||||||
"Contact",
|
"Contact",
|
||||||
"DeltaChat",
|
"DeltaChat",
|
||||||
"EventType",
|
"EventType",
|
||||||
"JsonRpcError",
|
|
||||||
"Message",
|
"Message",
|
||||||
"SpecialContactId",
|
"SpecialContactId",
|
||||||
"Rpc",
|
"Rpc",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import functools
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@@ -45,13 +44,8 @@ class AttrDict(dict):
|
|||||||
super().__setattr__(attr, val)
|
super().__setattr__(attr, val)
|
||||||
|
|
||||||
|
|
||||||
def _forever(_event: AttrDict) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def run_client_cli(
|
def run_client_cli(
|
||||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||||
until: Callable[[AttrDict], bool] = _forever,
|
|
||||||
argv: Optional[list] = None,
|
argv: Optional[list] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -61,11 +55,10 @@ def run_client_cli(
|
|||||||
"""
|
"""
|
||||||
from .client import Client
|
from .client import Client
|
||||||
|
|
||||||
_run_cli(Client, until, hooks, argv, **kwargs)
|
_run_cli(Client, hooks, argv, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def run_bot_cli(
|
def run_bot_cli(
|
||||||
until: Callable[[AttrDict], bool] = _forever,
|
|
||||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||||
argv: Optional[list] = None,
|
argv: Optional[list] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -76,12 +69,11 @@ def run_bot_cli(
|
|||||||
"""
|
"""
|
||||||
from .client import Bot
|
from .client import Bot
|
||||||
|
|
||||||
_run_cli(Bot, until, hooks, argv, **kwargs)
|
_run_cli(Bot, hooks, argv, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def _run_cli(
|
def _run_cli(
|
||||||
client_type: Type["Client"],
|
client_type: Type["Client"],
|
||||||
until: Callable[[AttrDict], bool] = _forever,
|
|
||||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||||
argv: Optional[list] = None,
|
argv: Optional[list] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -119,11 +111,11 @@ def _run_cli(
|
|||||||
kwargs={"email": args.email, "password": args.password},
|
kwargs={"email": args.email, "password": args.password},
|
||||||
)
|
)
|
||||||
configure_thread.start()
|
configure_thread.start()
|
||||||
client.run_until(until)
|
client.run_forever()
|
||||||
|
|
||||||
|
|
||||||
def extract_addr(text: str) -> str:
|
def extract_addr(text: str) -> str:
|
||||||
"""Extract email address from the given text."""
|
"""extract email address from the given text."""
|
||||||
match = re.match(r".*\((.+@.+)\)", text)
|
match = re.match(r".*\((.+@.+)\)", text)
|
||||||
if match:
|
if match:
|
||||||
text = match.group(1)
|
text = match.group(1)
|
||||||
@@ -132,7 +124,7 @@ def extract_addr(text: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]:
|
def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]:
|
||||||
"""Return image changed/deleted info from parsing the given system message text."""
|
"""return image changed/deleted info from parsing the given system message text."""
|
||||||
text = text.lower()
|
text = text.lower()
|
||||||
match = re.match(r"group image (changed|deleted) by (.+).", text)
|
match = re.match(r"group image (changed|deleted) by (.+).", text)
|
||||||
if match:
|
if match:
|
||||||
@@ -151,7 +143,7 @@ def parse_system_title_changed(text: str) -> Optional[Tuple[str, str]]:
|
|||||||
|
|
||||||
|
|
||||||
def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
||||||
"""Return add/remove info from parsing the given system message text.
|
"""return add/remove info from parsing the given system message text.
|
||||||
|
|
||||||
returns a (action, affected, actor) tuple.
|
returns a (action, affected, actor) tuple.
|
||||||
"""
|
"""
|
||||||
@@ -190,6 +182,9 @@ class futuremethod: # noqa: N801
|
|||||||
self._func = func
|
self._func = func
|
||||||
|
|
||||||
def __get__(self, instance, owner=None):
|
def __get__(self, instance, owner=None):
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
|
||||||
def future(*args):
|
def future(*args):
|
||||||
generator = self._func(instance, *args)
|
generator = self._func(instance, *args)
|
||||||
res = next(generator)
|
res = next(generator)
|
||||||
@@ -202,7 +197,6 @@ class futuremethod: # noqa: N801
|
|||||||
|
|
||||||
return f
|
return f
|
||||||
|
|
||||||
@functools.wraps(self._func)
|
|
||||||
def wrapper(*args):
|
def wrapper(*args):
|
||||||
f = future(*args)
|
f = future(*args)
|
||||||
return f()
|
return f()
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
"""Account module."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from ._utils import AttrDict, futuremethod
|
from ._utils import AttrDict, futuremethod
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
@@ -36,10 +34,7 @@ class Account:
|
|||||||
return next_event
|
return next_event
|
||||||
|
|
||||||
def clear_all_events(self):
|
def clear_all_events(self):
|
||||||
"""Remove all queued-up events for a given account.
|
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||||
|
|
||||||
Useful for tests.
|
|
||||||
"""
|
|
||||||
self._rpc.clear_all_events(self.id)
|
self._rpc.clear_all_events(self.id)
|
||||||
|
|
||||||
def remove(self) -> None:
|
def remove(self) -> None:
|
||||||
@@ -48,9 +43,7 @@ class Account:
|
|||||||
|
|
||||||
def clone(self) -> "Account":
|
def clone(self) -> "Account":
|
||||||
"""Clone given account.
|
"""Clone given account.
|
||||||
|
This uses backup-transfer via iroh, i.e. the 'Add second device' feature."""
|
||||||
This uses backup-transfer via iroh, i.e. the 'Add second device' feature.
|
|
||||||
"""
|
|
||||||
future = self._rpc.provide_backup.future(self.id)
|
future = self._rpc.provide_backup.future(self.id)
|
||||||
qr = self._rpc.get_backup_qr(self.id)
|
qr = self._rpc.get_backup_qr(self.id)
|
||||||
new_account = self.manager.add_account()
|
new_account = self.manager.add_account()
|
||||||
@@ -87,7 +80,7 @@ class Account:
|
|||||||
return self._rpc.get_config(self.id, key)
|
return self._rpc.get_config(self.id, key)
|
||||||
|
|
||||||
def update_config(self, **kwargs) -> None:
|
def update_config(self, **kwargs) -> None:
|
||||||
"""Update config values."""
|
"""update config values."""
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
self.set_config(key, value)
|
self.set_config(key, value)
|
||||||
|
|
||||||
@@ -106,12 +99,10 @@ class Account:
|
|||||||
"""Parse QR code contents.
|
"""Parse QR code contents.
|
||||||
|
|
||||||
This function takes the raw text scanned
|
This function takes the raw text scanned
|
||||||
and checks what can be done with it.
|
and checks what can be done with it."""
|
||||||
"""
|
|
||||||
return self._rpc.check_qr(self.id, qr)
|
return self._rpc.check_qr(self.id, qr)
|
||||||
|
|
||||||
def set_config_from_qr(self, qr: str):
|
def set_config_from_qr(self, qr: str):
|
||||||
"""Set configuration values from a QR code."""
|
|
||||||
self._rpc.set_config_from_qr(self.id, qr)
|
self._rpc.set_config_from_qr(self.id, qr)
|
||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
@@ -124,18 +115,9 @@ class Account:
|
|||||||
"""Add a new transport."""
|
"""Add a new transport."""
|
||||||
yield self._rpc.add_or_update_transport.future(self.id, params)
|
yield self._rpc.add_or_update_transport.future(self.id, params)
|
||||||
|
|
||||||
@futuremethod
|
|
||||||
def add_transport_from_qr(self, qr: str):
|
|
||||||
"""Add a new transport using a QR code."""
|
|
||||||
yield self._rpc.add_transport_from_qr.future(self.id, qr)
|
|
||||||
|
|
||||||
def delete_transport(self, addr: str):
|
|
||||||
"""Delete a transport."""
|
|
||||||
self._rpc.delete_transport(self.id, addr)
|
|
||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
def list_transports(self):
|
def list_transports(self):
|
||||||
"""Return 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."""
|
||||||
transports = yield self._rpc.list_transports.future(self.id)
|
transports = yield self._rpc.list_transports.future(self.id)
|
||||||
return transports
|
return transports
|
||||||
|
|
||||||
@@ -176,8 +158,7 @@ class Account:
|
|||||||
def import_vcard(self, vcard: str) -> list[Contact]:
|
def import_vcard(self, vcard: str) -> list[Contact]:
|
||||||
"""Import vCard.
|
"""Import vCard.
|
||||||
|
|
||||||
Return created or modified contacts in the order they appear in vCard.
|
Return created or modified contacts in the order they appear in vCard."""
|
||||||
"""
|
|
||||||
contact_ids = self._rpc.import_vcard_contents(self.id, vcard)
|
contact_ids = self._rpc.import_vcard_contents(self.id, vcard)
|
||||||
return [Contact(self, contact_id) for contact_id in contact_ids]
|
return [Contact(self, contact_id) for contact_id in contact_ids]
|
||||||
|
|
||||||
@@ -194,21 +175,7 @@ class Account:
|
|||||||
return Contact(self, contact_id)
|
return Contact(self, contact_id)
|
||||||
|
|
||||||
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||||
"""Looks up a known and unblocked contact with a given e-mail address.
|
"""Check if an e-mail address belongs to a known and unblocked contact."""
|
||||||
To get a list of all known and unblocked contacts, use contacts_get_contacts().
|
|
||||||
|
|
||||||
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
|
|
||||||
(e.g. an address-contact and a key-contact),
|
|
||||||
this looks up the most recently seen contact,
|
|
||||||
i.e. which contact is returned depends on which contact last sent a message.
|
|
||||||
If the user just clicked on a mailto: link, then this is the best thing you can do.
|
|
||||||
But **DO NOT** internally represent contacts by their email address
|
|
||||||
and do not use this function to look them up;
|
|
||||||
otherwise this function will sometimes look up the wrong contact.
|
|
||||||
Instead, you should internally represent contacts by their ids.
|
|
||||||
|
|
||||||
To validate an e-mail address independently of the contact database
|
|
||||||
use check_email_validity()."""
|
|
||||||
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||||
return contact_id and Contact(self, contact_id)
|
return contact_id and Contact(self, contact_id)
|
||||||
|
|
||||||
@@ -234,8 +201,8 @@ class Account:
|
|||||||
def get_contacts(
|
def get_contacts(
|
||||||
self,
|
self,
|
||||||
query: Optional[str] = None,
|
query: Optional[str] = None,
|
||||||
*,
|
|
||||||
with_self: bool = False,
|
with_self: bool = False,
|
||||||
|
verified_only: bool = False,
|
||||||
snapshot: bool = False,
|
snapshot: bool = False,
|
||||||
) -> Union[list[Contact], list[AttrDict]]:
|
) -> Union[list[Contact], list[AttrDict]]:
|
||||||
"""Get a filtered list of contacts.
|
"""Get a filtered list of contacts.
|
||||||
@@ -243,9 +210,12 @@ class Account:
|
|||||||
:param query: if a string is specified, only return contacts
|
:param query: if a string is specified, only return contacts
|
||||||
whose name or e-mail matches query.
|
whose name or e-mail matches query.
|
||||||
:param with_self: if True the self-contact is also included if it matches the query.
|
:param with_self: if True the self-contact is also included if it matches the query.
|
||||||
|
:param only_verified: if True only return verified contacts.
|
||||||
:param snapshot: If True return a list of contact snapshots instead of Contact instances.
|
:param snapshot: If True return a list of contact snapshots instead of Contact instances.
|
||||||
"""
|
"""
|
||||||
flags = 0
|
flags = 0
|
||||||
|
if verified_only:
|
||||||
|
flags |= ContactFlag.VERIFIED_ONLY
|
||||||
if with_self:
|
if with_self:
|
||||||
flags |= ContactFlag.ADD_SELF
|
flags |= ContactFlag.ADD_SELF
|
||||||
|
|
||||||
@@ -257,12 +227,12 @@ class Account:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def self_contact(self) -> Contact:
|
def self_contact(self) -> Contact:
|
||||||
"""Account's identity as a Contact."""
|
"""This account's identity as a Contact."""
|
||||||
return Contact(self, SpecialContactId.SELF)
|
return Contact(self, SpecialContactId.SELF)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_contact(self) -> Chat:
|
def device_contact(self) -> Chat:
|
||||||
"""Account's device contact."""
|
"""This account's device contact."""
|
||||||
return Contact(self, SpecialContactId.DEVICE)
|
return Contact(self, SpecialContactId.DEVICE)
|
||||||
|
|
||||||
def get_chatlist(
|
def get_chatlist(
|
||||||
@@ -308,51 +278,20 @@ class Account:
|
|||||||
chats.append(AttrDict(item))
|
chats.append(AttrDict(item))
|
||||||
return chats
|
return chats
|
||||||
|
|
||||||
def create_group(self, name: str) -> Chat:
|
def create_group(self, name: str, protect: bool = False) -> Chat:
|
||||||
"""Create a new group chat.
|
"""Create a new group chat.
|
||||||
|
|
||||||
After creation,
|
After creation, the group has only self-contact as member and is in unpromoted state.
|
||||||
the group has only self-contact as member one member (see `SpecialContactId.SELF`)
|
|
||||||
and is in _unpromoted_ state.
|
|
||||||
This means, you can add or remove members, change the name,
|
|
||||||
the group image and so on without messages being sent to all group members.
|
|
||||||
|
|
||||||
This changes as soon as the first message is sent to the group members
|
|
||||||
and the group becomes _promoted_.
|
|
||||||
After that, all changes are synced with all group members
|
|
||||||
by sending status message.
|
|
||||||
|
|
||||||
To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of a chat
|
|
||||||
(see `get_full_snapshot()` / `get_basic_snapshot()`).
|
|
||||||
This may be useful if you want to show some help for just created groups.
|
|
||||||
"""
|
"""
|
||||||
return Chat(self, self._rpc.create_group_chat(self.id, name, False))
|
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
||||||
|
|
||||||
def create_broadcast(self, name: str) -> Chat:
|
|
||||||
"""Create a new, outgoing **broadcast channel**
|
|
||||||
(called "Channel" in the UI).
|
|
||||||
|
|
||||||
Broadcast channels are similar to groups on the sending device,
|
|
||||||
however, recipients get the messages in a read-only chat
|
|
||||||
and will not see who the other members are.
|
|
||||||
|
|
||||||
Called `broadcast` here rather than `channel`,
|
|
||||||
because the word "channel" already appears a lot in the code,
|
|
||||||
which would make it hard to grep for it.
|
|
||||||
|
|
||||||
After creation, the chat contains no recipients and is in _unpromoted_ state;
|
|
||||||
see `create_group()` for more information on the unpromoted state.
|
|
||||||
|
|
||||||
Returns the created chat.
|
|
||||||
"""
|
|
||||||
return Chat(self, self._rpc.create_broadcast(self.id, name))
|
|
||||||
|
|
||||||
def get_chat_by_id(self, chat_id: int) -> Chat:
|
def get_chat_by_id(self, chat_id: int) -> Chat:
|
||||||
"""Return the Chat instance with the given ID."""
|
"""Return the Chat instance with the given ID."""
|
||||||
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 a Setup-Contact or Verified-Group-Invite 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.
|
||||||
@@ -391,7 +330,8 @@ class Account:
|
|||||||
"""Return the list of fresh messages, newest messages first.
|
"""Return the list of fresh messages, newest messages first.
|
||||||
|
|
||||||
This call is intended for displaying notifications.
|
This call is intended for displaying notifications.
|
||||||
If you are writing a bot, process "incoming message" events instead.
|
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
|
||||||
|
to process oldest messages first.
|
||||||
"""
|
"""
|
||||||
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
||||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||||
@@ -401,18 +341,9 @@ class Account:
|
|||||||
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
||||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||||
|
|
||||||
@futuremethod
|
|
||||||
def wait_next_messages(self) -> list[Message]:
|
def wait_next_messages(self) -> list[Message]:
|
||||||
"""(deprecated) Wait for new messages and return a list of them. Meant for bots.
|
"""Wait for new messages and return a list of them."""
|
||||||
|
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
||||||
Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
|
|
||||||
even if it is not fully downloaded yet.
|
|
||||||
The bot needs to wait for the message to be fully downloaded.
|
|
||||||
Since this is usually not the desired behavior,
|
|
||||||
bots should instead use the `EventType.INCOMING_MSG`
|
|
||||||
event for getting notified about new messages.
|
|
||||||
"""
|
|
||||||
next_msg_ids = yield self._rpc.wait_next_msgs.future(self.id)
|
|
||||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||||
|
|
||||||
def wait_for_incoming_msg_event(self):
|
def wait_for_incoming_msg_event(self):
|
||||||
@@ -427,40 +358,37 @@ class Account:
|
|||||||
"""Wait for messages noticed event and return it."""
|
"""Wait for messages noticed event and return it."""
|
||||||
return self.wait_for_event(EventType.MSGS_NOTICED)
|
return self.wait_for_event(EventType.MSGS_NOTICED)
|
||||||
|
|
||||||
def wait_for_msg(self, event_type) -> Message:
|
|
||||||
"""Wait for an event about the message.
|
|
||||||
|
|
||||||
Consumes all events before the matching event.
|
|
||||||
Returns a message corresponding to the msg_id field of the event.
|
|
||||||
"""
|
|
||||||
event = self.wait_for_event(event_type)
|
|
||||||
return self.get_message_by_id(event.msg_id)
|
|
||||||
|
|
||||||
def wait_for_incoming_msg(self):
|
def wait_for_incoming_msg(self):
|
||||||
"""Wait for incoming message and return it.
|
"""Wait for incoming message and return it.
|
||||||
|
|
||||||
Consumes all events before the next incoming message event.
|
Consumes all events before the next incoming message event."""
|
||||||
"""
|
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
|
||||||
return self.wait_for_msg(EventType.INCOMING_MSG)
|
|
||||||
|
|
||||||
def wait_for_securejoin_inviter_success(self):
|
def wait_for_securejoin_inviter_success(self):
|
||||||
"""Wait until SecureJoin process finishes successfully on the inviter side."""
|
|
||||||
while True:
|
while True:
|
||||||
event = self.wait_for_event()
|
event = self.wait_for_event()
|
||||||
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||||
break
|
break
|
||||||
|
|
||||||
def wait_for_securejoin_joiner_success(self):
|
def wait_for_securejoin_joiner_success(self):
|
||||||
"""Wait until SecureJoin process finishes successfully on the joiner side."""
|
|
||||||
while True:
|
while True:
|
||||||
event = self.wait_for_event()
|
event = self.wait_for_event()
|
||||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||||
break
|
break
|
||||||
|
|
||||||
def wait_for_reactions_changed(self):
|
def wait_for_reactions_changed(self):
|
||||||
"""Wait for reaction change event."""
|
|
||||||
return self.wait_for_event(EventType.REACTIONS_CHANGED)
|
return self.wait_for_event(EventType.REACTIONS_CHANGED)
|
||||||
|
|
||||||
|
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
||||||
|
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||||
|
warn(
|
||||||
|
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
fresh_msg_ids = sorted(self._rpc.get_fresh_msgs(self.id))
|
||||||
|
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||||
|
|
||||||
def export_backup(self, path, passphrase: str = "") -> None:
|
def export_backup(self, path, passphrase: str = "") -> None:
|
||||||
"""Export backup."""
|
"""Export backup."""
|
||||||
self._rpc.export_backup(self.id, str(path), passphrase)
|
self._rpc.export_backup(self.id, str(path), passphrase)
|
||||||
@@ -479,11 +407,6 @@ class Account:
|
|||||||
passphrase = "" # Importing passphrase-protected keys is currently not supported.
|
passphrase = "" # Importing passphrase-protected keys is currently not supported.
|
||||||
self._rpc.import_self_keys(self.id, str(path), passphrase)
|
self._rpc.import_self_keys(self.id, str(path), passphrase)
|
||||||
|
|
||||||
def ice_servers(self) -> list:
|
def initiate_autocrypt_key_transfer(self) -> None:
|
||||||
"""Return ICE servers for WebRTC configuration."""
|
"""Send Autocrypt Setup Message."""
|
||||||
ice_servers_json = self._rpc.ice_servers(self.id)
|
return self._rpc.initiate_autocrypt_key_transfer(self.id)
|
||||||
return json.loads(ice_servers_json)
|
|
||||||
|
|
||||||
def is_sending_locations(self) -> bool:
|
|
||||||
"""Return True if sending locations to any chat."""
|
|
||||||
return self._rpc.is_sending_locations(self.id)
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"""Chat module."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
@@ -91,8 +89,7 @@ class Chat:
|
|||||||
def set_ephemeral_timer(self, timer: int) -> None:
|
def set_ephemeral_timer(self, timer: int) -> None:
|
||||||
"""Set ephemeral timer of this chat in seconds.
|
"""Set ephemeral timer of this chat in seconds.
|
||||||
|
|
||||||
0 means the timer is disabled, use 1 for immediate deletion.
|
0 means the timer is disabled, use 1 for immediate deletion."""
|
||||||
"""
|
|
||||||
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||||
|
|
||||||
def get_encryption_info(self) -> str:
|
def get_encryption_info(self) -> str:
|
||||||
@@ -164,15 +161,10 @@ class Chat:
|
|||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
def send_sticker(self, path: str) -> Message:
|
def send_sticker(self, path: str) -> Message:
|
||||||
"""Deprecated as of 2026-04; use `send_message` with `Viewtype.STICKER` instead."""
|
"""Send an sticker and return the resulting Message instance."""
|
||||||
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
def resend_messages(self, messages: list[Message]) -> None:
|
|
||||||
"""Resend a list of messages to this chat."""
|
|
||||||
msg_ids = [msg.id for msg in messages]
|
|
||||||
self._rpc.resend_messages(self.account.id, msg_ids)
|
|
||||||
|
|
||||||
def forward_messages(self, messages: list[Message]) -> None:
|
def forward_messages(self, messages: list[Message]) -> None:
|
||||||
"""Forward a list of messages to this chat."""
|
"""Forward a list of messages to this chat."""
|
||||||
msg_ids = [msg.id for msg in messages]
|
msg_ids = [msg.id for msg in messages]
|
||||||
@@ -206,29 +198,23 @@ class Chat:
|
|||||||
snapshot["message"] = Message(self.account, snapshot.id)
|
snapshot["message"] = Message(self.account, snapshot.id)
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
def get_messages(self, add_daymarker: bool = False) -> list[Message]:
|
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
|
||||||
"""Get the list of messages in this chat."""
|
"""get the list of messages in this chat."""
|
||||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, False, add_daymarker)
|
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||||
|
|
||||||
def get_fresh_message_count(self) -> int:
|
def get_fresh_message_count(self) -> int:
|
||||||
"""Get number of fresh messages in this chat."""
|
"""Get number of fresh messages in this chat"""
|
||||||
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
||||||
|
|
||||||
def mark_noticed(self) -> None:
|
def mark_noticed(self) -> None:
|
||||||
"""Mark all messages in this chat as noticed."""
|
"""Mark all messages in this chat as noticed."""
|
||||||
self._rpc.marknoticed_chat(self.account.id, self.id)
|
self._rpc.marknoticed_chat(self.account.id, self.id)
|
||||||
|
|
||||||
def mark_fresh(self) -> None:
|
def add_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||||
"""Mark the last incoming message in the chat as fresh."""
|
|
||||||
self._rpc.markfresh_chat(self.account.id, self.id)
|
|
||||||
|
|
||||||
def add_contact(self, *contact: Union[int, str, Contact, "Account"]) -> None:
|
|
||||||
"""Add contacts to this group."""
|
"""Add contacts to this group."""
|
||||||
from .account import Account
|
|
||||||
|
|
||||||
for cnt in contact:
|
for cnt in contact:
|
||||||
if isinstance(cnt, (str, Account)):
|
if isinstance(cnt, str):
|
||||||
contact_id = self.account.create_contact(cnt).id
|
contact_id = self.account.create_contact(cnt).id
|
||||||
elif not isinstance(cnt, int):
|
elif not isinstance(cnt, int):
|
||||||
contact_id = cnt.id
|
contact_id = cnt.id
|
||||||
@@ -236,12 +222,10 @@ class Chat:
|
|||||||
contact_id = cnt
|
contact_id = cnt
|
||||||
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
|
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
|
||||||
|
|
||||||
def remove_contact(self, *contact: Union[int, str, Contact, "Account"]) -> None:
|
def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||||
"""Remove members from this group."""
|
"""Remove members from this group."""
|
||||||
from .account import Account
|
|
||||||
|
|
||||||
for cnt in contact:
|
for cnt in contact:
|
||||||
if isinstance(cnt, (str, Account)):
|
if isinstance(cnt, str):
|
||||||
contact_id = self.account.create_contact(cnt).id
|
contact_id = self.account.create_contact(cnt).id
|
||||||
elif not isinstance(cnt, int):
|
elif not isinstance(cnt, int):
|
||||||
contact_id = cnt.id
|
contact_id = cnt.id
|
||||||
@@ -257,10 +241,6 @@ class Chat:
|
|||||||
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]
|
||||||
|
|
||||||
def num_contacts(self) -> int:
|
|
||||||
"""Return number of contacts in this chat."""
|
|
||||||
return len(self.get_contacts())
|
|
||||||
|
|
||||||
def get_past_contacts(self) -> list[Contact]:
|
def get_past_contacts(self) -> list[Contact]:
|
||||||
"""Get past contacts for this chat."""
|
"""Get past contacts for this chat."""
|
||||||
past_contacts = self._rpc.get_past_chat_contacts(self.account.id, self.id)
|
past_contacts = self._rpc.get_past_chat_contacts(self.account.id, self.id)
|
||||||
@@ -277,16 +257,6 @@ class Chat:
|
|||||||
"""Remove profile image of this chat."""
|
"""Remove profile image of this chat."""
|
||||||
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
||||||
|
|
||||||
def send_locations(self, seconds) -> None:
|
|
||||||
"""Enable location streaming in the chat for the given number of seconds.
|
|
||||||
|
|
||||||
Pass 0 to disable location streaming."""
|
|
||||||
self._rpc.send_locations_to_chat(self.account.id, self.id, seconds)
|
|
||||||
|
|
||||||
def is_sending_locations(self) -> bool:
|
|
||||||
"""Return True if sending locations to this chat."""
|
|
||||||
return self._rpc.is_sending_locations_to_chat(self.account.id, self.id)
|
|
||||||
|
|
||||||
def get_locations(
|
def get_locations(
|
||||||
self,
|
self,
|
||||||
contact: Optional[Contact] = None,
|
contact: Optional[Contact] = None,
|
||||||
@@ -316,8 +286,3 @@ class Chat:
|
|||||||
f.write(vcard.encode())
|
f.write(vcard.encode())
|
||||||
f.flush()
|
f.flush()
|
||||||
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
|
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
|
||||||
|
|
||||||
def place_outgoing_call(self, place_call_info: str, has_video_initially: bool) -> Message:
|
|
||||||
"""Starts an outgoing call."""
|
|
||||||
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info, has_video_initially)
|
|
||||||
return Message(self.account, msg_id)
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from typing import (
|
|||||||
|
|
||||||
from ._utils import (
|
from ._utils import (
|
||||||
AttrDict,
|
AttrDict,
|
||||||
_forever,
|
|
||||||
parse_system_add_remove,
|
parse_system_add_remove,
|
||||||
parse_system_image_changed,
|
parse_system_image_changed,
|
||||||
parse_system_title_changed,
|
parse_system_title_changed,
|
||||||
@@ -49,7 +48,6 @@ class Client:
|
|||||||
self.add_hooks(hooks or [])
|
self.add_hooks(hooks or [])
|
||||||
|
|
||||||
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
|
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
|
||||||
"""Register multiple hooks."""
|
|
||||||
for hook, event in hooks:
|
for hook, event in hooks:
|
||||||
self.add_hook(hook, event)
|
self.add_hook(hook, event)
|
||||||
|
|
||||||
@@ -79,41 +77,31 @@ class Client:
|
|||||||
self._hooks.get(type(event), set()).remove((hook, event))
|
self._hooks.get(type(event), set()).remove((hook, event))
|
||||||
|
|
||||||
def is_configured(self) -> bool:
|
def is_configured(self) -> bool:
|
||||||
"""Return True if the client is configured."""
|
|
||||||
return self.account.is_configured()
|
return self.account.is_configured()
|
||||||
|
|
||||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||||
"""Configure the client."""
|
self.account.set_config("addr", email)
|
||||||
|
self.account.set_config("mail_pw", password)
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
self.account.set_config(key, value)
|
self.account.set_config(key, value)
|
||||||
params = {"addr": email, "password": password}
|
self.account.configure()
|
||||||
self.account.add_or_update_transport(params)
|
|
||||||
self.logger.debug("Account configured")
|
self.logger.debug("Account configured")
|
||||||
|
|
||||||
def run_forever(self) -> None:
|
def run_forever(self) -> None:
|
||||||
"""Process events forever."""
|
"""Process events forever."""
|
||||||
self.run_until(_forever)
|
self.run_until(lambda _: False)
|
||||||
|
|
||||||
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
|
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
|
||||||
"""Start the event processing loop."""
|
"""Process events until the given callable evaluates to True.
|
||||||
|
|
||||||
|
The callable should accept an AttrDict object representing the
|
||||||
|
last processed event. The event is returned when the callable
|
||||||
|
evaluates to True.
|
||||||
|
"""
|
||||||
self.logger.debug("Listening to incoming events...")
|
self.logger.debug("Listening to incoming events...")
|
||||||
if self.is_configured():
|
if self.is_configured():
|
||||||
self.account.start_io()
|
self.account.start_io()
|
||||||
self._process_messages() # Process old messages.
|
self._process_messages() # Process old messages.
|
||||||
return self._process_events(until_func=func) # Loop over incoming events
|
|
||||||
|
|
||||||
def _process_events(
|
|
||||||
self,
|
|
||||||
until_func: Callable[[AttrDict], bool] = _forever,
|
|
||||||
until_event: EventType = False,
|
|
||||||
) -> AttrDict:
|
|
||||||
"""Process events until the given callable evaluates to True,
|
|
||||||
or until a certain event happens.
|
|
||||||
|
|
||||||
The until_func callable should accept an AttrDict object representing
|
|
||||||
the last processed event. The event is returned when the callable
|
|
||||||
evaluates to True.
|
|
||||||
"""
|
|
||||||
while True:
|
while True:
|
||||||
event = self.account.wait_for_event()
|
event = self.account.wait_for_event()
|
||||||
event["kind"] = EventType(event.kind)
|
event["kind"] = EventType(event.kind)
|
||||||
@@ -122,13 +110,10 @@ class Client:
|
|||||||
if event.kind == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
self._process_messages()
|
self._process_messages()
|
||||||
|
|
||||||
stop = until_func(event)
|
stop = func(event)
|
||||||
if stop:
|
if stop:
|
||||||
return event
|
return event
|
||||||
|
|
||||||
if event.kind == until_event:
|
|
||||||
return event
|
|
||||||
|
|
||||||
def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
||||||
for hook, evfilter in self._hooks.get(filter_type, []):
|
for hook, evfilter in self._hooks.get(filter_type, []):
|
||||||
if evfilter.filter(event):
|
if evfilter.filter(event):
|
||||||
@@ -213,6 +198,5 @@ class Bot(Client):
|
|||||||
"""Simple bot implementation that listens to events of a single account."""
|
"""Simple bot implementation that listens to events of a single account."""
|
||||||
|
|
||||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||||
"""Configure the bot."""
|
|
||||||
kwargs.setdefault("bot", "1")
|
kwargs.setdefault("bot", "1")
|
||||||
super().configure(email, password, **kwargs)
|
super().configure(email, password, **kwargs)
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
"""Constants module."""
|
|
||||||
|
|
||||||
from enum import Enum, IntEnum
|
from enum import Enum, IntEnum
|
||||||
|
|
||||||
COMMAND_PREFIX = "/"
|
COMMAND_PREFIX = "/"
|
||||||
|
|
||||||
|
|
||||||
class ContactFlag(IntEnum):
|
class ContactFlag(IntEnum):
|
||||||
"""Bit flags for get_contacts() method."""
|
VERIFIED_ONLY = 0x01
|
||||||
|
|
||||||
ADD_SELF = 0x02
|
ADD_SELF = 0x02
|
||||||
ADDRESS = 0x04
|
|
||||||
|
|
||||||
|
|
||||||
class ChatlistFlag(IntEnum):
|
class ChatlistFlag(IntEnum):
|
||||||
"""Bit flags for get_chatlist() method."""
|
|
||||||
|
|
||||||
ARCHIVED_ONLY = 0x01
|
ARCHIVED_ONLY = 0x01
|
||||||
NO_SPECIALS = 0x02
|
NO_SPECIALS = 0x02
|
||||||
ADD_ALLDONE_HINT = 0x04
|
ADD_ALLDONE_HINT = 0x04
|
||||||
@@ -22,8 +16,6 @@ class ChatlistFlag(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class SpecialContactId(IntEnum):
|
class SpecialContactId(IntEnum):
|
||||||
"""Special contact IDs."""
|
|
||||||
|
|
||||||
SELF = 1
|
SELF = 1
|
||||||
INFO = 2 # centered messages as "member added", used in all chats
|
INFO = 2 # centered messages as "member added", used in all chats
|
||||||
DEVICE = 5 # messages "update info" in the device-chat
|
DEVICE = 5 # messages "update info" in the device-chat
|
||||||
@@ -31,7 +23,7 @@ class SpecialContactId(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class EventType(str, Enum):
|
class EventType(str, Enum):
|
||||||
"""Core event types."""
|
"""Core event types"""
|
||||||
|
|
||||||
INFO = "Info"
|
INFO = "Info"
|
||||||
SMTP_CONNECTED = "SmtpConnected"
|
SMTP_CONNECTED = "SmtpConnected"
|
||||||
@@ -73,18 +65,13 @@ class EventType(str, Enum):
|
|||||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||||
ACCOUNTS_CHANGED = "AccountsChanged"
|
ACCOUNTS_CHANGED = "AccountsChanged"
|
||||||
ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged"
|
ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged"
|
||||||
INCOMING_CALL = "IncomingCall"
|
|
||||||
INCOMING_CALL_ACCEPTED = "IncomingCallAccepted"
|
|
||||||
OUTGOING_CALL_ACCEPTED = "OutgoingCallAccepted"
|
|
||||||
CALL_ENDED = "CallEnded"
|
|
||||||
CONFIG_SYNCED = "ConfigSynced"
|
CONFIG_SYNCED = "ConfigSynced"
|
||||||
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
||||||
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
|
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
|
||||||
TRANSPORTS_MODIFIED = "TransportsModified"
|
|
||||||
|
|
||||||
|
|
||||||
class ChatId(IntEnum):
|
class ChatId(IntEnum):
|
||||||
"""Special chat IDs."""
|
"""Special chat ids"""
|
||||||
|
|
||||||
TRASH = 3
|
TRASH = 3
|
||||||
ARCHIVED_LINK = 6
|
ARCHIVED_LINK = 6
|
||||||
@@ -92,46 +79,18 @@ class ChatId(IntEnum):
|
|||||||
LAST_SPECIAL = 9
|
LAST_SPECIAL = 9
|
||||||
|
|
||||||
|
|
||||||
class ChatType(str, Enum):
|
class ChatType(IntEnum):
|
||||||
"""Chat type."""
|
"""Chat types"""
|
||||||
|
|
||||||
SINGLE = "Single"
|
UNDEFINED = 0
|
||||||
"""1:1 chat, i.e. a direct chat with a single contact"""
|
SINGLE = 100
|
||||||
|
GROUP = 120
|
||||||
GROUP = "Group"
|
MAILINGLIST = 140
|
||||||
|
BROADCAST = 160
|
||||||
MAILINGLIST = "Mailinglist"
|
|
||||||
|
|
||||||
OUT_BROADCAST = "OutBroadcast"
|
|
||||||
"""Outgoing broadcast channel, called "Channel" in the UI.
|
|
||||||
|
|
||||||
The user can send into this channel,
|
|
||||||
and all recipients will receive messages
|
|
||||||
in an `IN_BROADCAST`.
|
|
||||||
|
|
||||||
Called `broadcast` here rather than `channel`,
|
|
||||||
because the word "channel" already appears a lot in the code,
|
|
||||||
which would make it hard to grep for it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
IN_BROADCAST = "InBroadcast"
|
|
||||||
"""Incoming broadcast channel, called "Channel" in the UI.
|
|
||||||
|
|
||||||
This channel is read-only,
|
|
||||||
and we do not know who the other recipients are.
|
|
||||||
|
|
||||||
This is similar to a `MAILINGLIST`,
|
|
||||||
with the main difference being that
|
|
||||||
`IN_BROADCAST`s are encrypted.
|
|
||||||
|
|
||||||
Called `broadcast` here rather than `channel`,
|
|
||||||
because the word "channel" already appears a lot in the code,
|
|
||||||
which would make it hard to grep for it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class ChatVisibility(str, Enum):
|
class ChatVisibility(str, Enum):
|
||||||
"""Chat visibility types."""
|
"""Chat visibility types"""
|
||||||
|
|
||||||
NORMAL = "Normal"
|
NORMAL = "Normal"
|
||||||
ARCHIVED = "Archived"
|
ARCHIVED = "Archived"
|
||||||
@@ -139,7 +98,7 @@ class ChatVisibility(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class DownloadState(str, Enum):
|
class DownloadState(str, Enum):
|
||||||
"""Message download state."""
|
"""Message download state"""
|
||||||
|
|
||||||
DONE = "Done"
|
DONE = "Done"
|
||||||
AVAILABLE = "Available"
|
AVAILABLE = "Available"
|
||||||
@@ -159,6 +118,7 @@ class ViewType(str, Enum):
|
|||||||
VOICE = "Voice"
|
VOICE = "Voice"
|
||||||
VIDEO = "Video"
|
VIDEO = "Video"
|
||||||
FILE = "File"
|
FILE = "File"
|
||||||
|
VIDEOCHAT_INVITATION = "VideochatInvitation"
|
||||||
WEBXDC = "Webxdc"
|
WEBXDC = "Webxdc"
|
||||||
VCARD = "Vcard"
|
VCARD = "Vcard"
|
||||||
|
|
||||||
@@ -190,6 +150,7 @@ class MessageState(IntEnum):
|
|||||||
IN_FRESH = 10
|
IN_FRESH = 10
|
||||||
IN_NOTICED = 13
|
IN_NOTICED = 13
|
||||||
IN_SEEN = 16
|
IN_SEEN = 16
|
||||||
|
OUT_PREPARING = 18
|
||||||
OUT_DRAFT = 19
|
OUT_DRAFT = 19
|
||||||
OUT_PENDING = 20
|
OUT_PENDING = 20
|
||||||
OUT_FAILED = 24
|
OUT_FAILED = 24
|
||||||
@@ -198,14 +159,14 @@ class MessageState(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class MessageId(IntEnum):
|
class MessageId(IntEnum):
|
||||||
"""Special message IDs."""
|
"""Special message ids"""
|
||||||
|
|
||||||
DAYMARKER = 9
|
DAYMARKER = 9
|
||||||
LAST_SPECIAL = 9
|
LAST_SPECIAL = 9
|
||||||
|
|
||||||
|
|
||||||
class CertificateChecks(IntEnum):
|
class CertificateChecks(IntEnum):
|
||||||
"""Certificate checks mode."""
|
"""Certificate checks mode"""
|
||||||
|
|
||||||
AUTOMATIC = 0
|
AUTOMATIC = 0
|
||||||
STRICT = 1
|
STRICT = 1
|
||||||
@@ -213,7 +174,7 @@ class CertificateChecks(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class Connectivity(IntEnum):
|
class Connectivity(IntEnum):
|
||||||
"""Connectivity states."""
|
"""Connectivity states"""
|
||||||
|
|
||||||
NOT_CONNECTED = 1000
|
NOT_CONNECTED = 1000
|
||||||
CONNECTING = 2000
|
CONNECTING = 2000
|
||||||
@@ -222,7 +183,7 @@ class Connectivity(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class KeyGenType(IntEnum):
|
class KeyGenType(IntEnum):
|
||||||
"""Type of the key to generate."""
|
"""Type of the key to generate"""
|
||||||
|
|
||||||
DEFAULT = 0
|
DEFAULT = 0
|
||||||
RSA2048 = 1
|
RSA2048 = 1
|
||||||
@@ -232,21 +193,21 @@ class KeyGenType(IntEnum):
|
|||||||
|
|
||||||
# "Lp" means "login parameters"
|
# "Lp" means "login parameters"
|
||||||
class LpAuthFlag(IntEnum):
|
class LpAuthFlag(IntEnum):
|
||||||
"""Authorization flags."""
|
"""Authorization flags"""
|
||||||
|
|
||||||
OAUTH2 = 0x2
|
OAUTH2 = 0x2
|
||||||
NORMAL = 0x4
|
NORMAL = 0x4
|
||||||
|
|
||||||
|
|
||||||
class MediaQuality(IntEnum):
|
class MediaQuality(IntEnum):
|
||||||
"""Media quality setting."""
|
"""Media quality setting"""
|
||||||
|
|
||||||
BALANCED = 0
|
BALANCED = 0
|
||||||
WORSE = 1
|
WORSE = 1
|
||||||
|
|
||||||
|
|
||||||
class ProviderStatus(IntEnum):
|
class ProviderStatus(IntEnum):
|
||||||
"""Provider status according to manual testing."""
|
"""Provider status according to manual testing"""
|
||||||
|
|
||||||
OK = 1
|
OK = 1
|
||||||
PREPARATION = 2
|
PREPARATION = 2
|
||||||
@@ -254,7 +215,7 @@ class ProviderStatus(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class PushNotifyState(IntEnum):
|
class PushNotifyState(IntEnum):
|
||||||
"""Push notifications state."""
|
"""Push notifications state"""
|
||||||
|
|
||||||
NOT_CONNECTED = 0
|
NOT_CONNECTED = 0
|
||||||
HEARTBEAT = 1
|
HEARTBEAT = 1
|
||||||
@@ -262,7 +223,7 @@ class PushNotifyState(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class ShowEmails(IntEnum):
|
class ShowEmails(IntEnum):
|
||||||
"""Show emails mode."""
|
"""Show emails mode"""
|
||||||
|
|
||||||
OFF = 0
|
OFF = 0
|
||||||
ACCEPTED_CONTACTS = 1
|
ACCEPTED_CONTACTS = 1
|
||||||
@@ -270,9 +231,17 @@ class ShowEmails(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class SocketSecurity(IntEnum):
|
class SocketSecurity(IntEnum):
|
||||||
"""Socket security."""
|
"""Socket security"""
|
||||||
|
|
||||||
AUTOMATIC = 0
|
AUTOMATIC = 0
|
||||||
SSL = 1
|
SSL = 1
|
||||||
STARTTLS = 2
|
STARTTLS = 2
|
||||||
PLAIN = 3
|
PLAIN = 3
|
||||||
|
|
||||||
|
|
||||||
|
class VideochatType(IntEnum):
|
||||||
|
"""Video chat URL type"""
|
||||||
|
|
||||||
|
UNKNOWN = 0
|
||||||
|
BASICWEBRTC = 1
|
||||||
|
JITSI = 2
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"""Contact module."""
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -13,7 +11,8 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Contact:
|
class Contact:
|
||||||
"""Contact API.
|
"""
|
||||||
|
Contact API.
|
||||||
|
|
||||||
Essentially a wrapper for RPC, account ID and a contact ID.
|
Essentially a wrapper for RPC, account ID and a contact ID.
|
||||||
"""
|
"""
|
||||||
@@ -37,14 +36,17 @@ class Contact:
|
|||||||
"""Delete contact."""
|
"""Delete contact."""
|
||||||
self._rpc.delete_contact(self.account.id, self.id)
|
self._rpc.delete_contact(self.account.id, self.id)
|
||||||
|
|
||||||
|
def reset_encryption(self) -> None:
|
||||||
|
"""Reset contact encryption."""
|
||||||
|
self._rpc.reset_contact_encryption(self.account.id, self.id)
|
||||||
|
|
||||||
def set_name(self, name: str) -> None:
|
def set_name(self, name: str) -> None:
|
||||||
"""Change the name of this contact."""
|
"""Change the name of this contact."""
|
||||||
self._rpc.change_contact_name(self.account.id, self.id, name)
|
self._rpc.change_contact_name(self.account.id, self.id, name)
|
||||||
|
|
||||||
def get_encryption_info(self) -> str:
|
def get_encryption_info(self) -> str:
|
||||||
"""Get a multi-line encryption info.
|
"""Get a multi-line encryption info, containing your fingerprint and
|
||||||
|
the fingerprint of the contact.
|
||||||
Encryption info contains your fingerprint and the fingerprint of the contact.
|
|
||||||
"""
|
"""
|
||||||
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
||||||
|
|
||||||
@@ -64,5 +66,4 @@ class Contact:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def make_vcard(self) -> str:
|
def make_vcard(self) -> str:
|
||||||
"""Make a vCard for the contact."""
|
|
||||||
return self.account.make_vcard([self])
|
return self.account.make_vcard([self])
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
"""Account manager module."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ._utils import AttrDict, futuremethod
|
from ._utils import AttrDict
|
||||||
from .account import Account
|
from .account import Account
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -12,13 +10,12 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class DeltaChat:
|
class DeltaChat:
|
||||||
"""Delta Chat accounts manager.
|
"""
|
||||||
|
Delta Chat accounts manager.
|
||||||
This is the root of the object oriented API.
|
This is the root of the object oriented API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, rpc: "Rpc") -> None:
|
def __init__(self, rpc: "Rpc") -> None:
|
||||||
"""Initialize account manager."""
|
|
||||||
self.rpc = rpc
|
self.rpc = rpc
|
||||||
|
|
||||||
def add_account(self) -> Account:
|
def add_account(self) -> Account:
|
||||||
@@ -39,17 +36,10 @@ class DeltaChat:
|
|||||||
"""Stop the I/O of all accounts."""
|
"""Stop the I/O of all accounts."""
|
||||||
self.rpc.stop_io_for_all_accounts()
|
self.rpc.stop_io_for_all_accounts()
|
||||||
|
|
||||||
@futuremethod
|
|
||||||
def background_fetch(self, timeout_in_seconds: int) -> None:
|
|
||||||
"""Run background fetch for all accounts."""
|
|
||||||
yield self.rpc.background_fetch.future(timeout_in_seconds)
|
|
||||||
|
|
||||||
def stop_background_fetch(self) -> None:
|
|
||||||
"""Stop ongoing background fetch."""
|
|
||||||
self.rpc.stop_background_fetch()
|
|
||||||
|
|
||||||
def maybe_network(self) -> None:
|
def maybe_network(self) -> None:
|
||||||
"""Indicate that the network conditions might have changed."""
|
"""Indicate that the network likely has come back or just that the network
|
||||||
|
conditions might have changed.
|
||||||
|
"""
|
||||||
self.rpc.maybe_network()
|
self.rpc.maybe_network()
|
||||||
|
|
||||||
def get_system_info(self) -> AttrDict:
|
def get_system_info(self) -> AttrDict:
|
||||||
@@ -59,11 +49,3 @@ class DeltaChat:
|
|||||||
def set_translations(self, translations: dict[str, str]) -> None:
|
def set_translations(self, translations: dict[str, str]) -> None:
|
||||||
"""Set stock translation strings."""
|
"""Set stock translation strings."""
|
||||||
self.rpc.set_stock_strings(translations)
|
self.rpc.set_stock_strings(translations)
|
||||||
|
|
||||||
def set_location(self, latitude, longitude, accuracy) -> bool:
|
|
||||||
"""Set location, return True if location streaming should continue."""
|
|
||||||
return self.rpc.set_location(latitude, longitude, accuracy)
|
|
||||||
|
|
||||||
def stop_sending_locations(self) -> None:
|
|
||||||
"""Stop sending locations to all chats."""
|
|
||||||
return self.rpc.stop_sending_locations()
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class EventFilter(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
"""Object's unique hash."""
|
"""Object's unique hash"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __eq__(self, other) -> bool:
|
def __eq__(self, other) -> bool:
|
||||||
@@ -52,7 +52,9 @@ class EventFilter(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def filter(self, event):
|
def filter(self, event):
|
||||||
"""Return True-like value if the event passed the filter."""
|
"""Return True-like value if the event passed the filter and should be
|
||||||
|
used, or False-like value otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class RawEvent(EventFilter):
|
class RawEvent(EventFilter):
|
||||||
@@ -80,17 +82,31 @@ class RawEvent(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
"""Filter an event.
|
|
||||||
|
|
||||||
Return true if the event should be processed.
|
|
||||||
"""
|
|
||||||
if self.types and event.kind not in self.types:
|
if self.types and event.kind not in self.types:
|
||||||
return False
|
return False
|
||||||
return self._call_func(event)
|
return self._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class NewMessage(EventFilter):
|
class NewMessage(EventFilter):
|
||||||
"""Matches whenever a new message arrives."""
|
"""Matches whenever a new message arrives.
|
||||||
|
|
||||||
|
Warning: registering a handler for this event will cause the messages
|
||||||
|
to be marked as read. Its usage is mainly intended for bots.
|
||||||
|
|
||||||
|
:param pattern: if set, this Pattern will be used to filter the message by its text
|
||||||
|
content.
|
||||||
|
:param command: If set, only match messages with the given command (ex. /help).
|
||||||
|
Setting this property implies `is_info==False`.
|
||||||
|
:param is_bot: If set to True only match messages sent by bots, if set to None
|
||||||
|
match messages from bots and users. If omitted or set to False
|
||||||
|
only messages from users will be matched.
|
||||||
|
:param is_info: If set to True only match info/system messages, if set to False
|
||||||
|
only match messages that are not info/system messages. If omitted
|
||||||
|
info/system messages as well as normal messages will be matched.
|
||||||
|
:param func: A Callable function that should accept the event as input
|
||||||
|
parameter, and return a bool value indicating whether the event
|
||||||
|
should be dispatched or not.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -105,25 +121,6 @@ class NewMessage(EventFilter):
|
|||||||
is_info: Optional[bool] = None,
|
is_info: Optional[bool] = None,
|
||||||
func: Optional[Callable[["AttrDict"], bool]] = None,
|
func: Optional[Callable[["AttrDict"], bool]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a new message filter.
|
|
||||||
|
|
||||||
Warning: registering a handler for this event will cause the messages
|
|
||||||
to be marked as read. Its usage is mainly intended for bots.
|
|
||||||
|
|
||||||
:param pattern: if set, this Pattern will be used to filter the message by its text
|
|
||||||
content.
|
|
||||||
:param command: If set, only match messages with the given command (ex. /help).
|
|
||||||
Setting this property implies `is_info==False`.
|
|
||||||
:param is_bot: If set to True only match messages sent by bots, if set to None
|
|
||||||
match messages from bots and users. If omitted or set to False
|
|
||||||
only messages from users will be matched.
|
|
||||||
:param is_info: If set to True only match info/system messages, if set to False
|
|
||||||
only match messages that are not info/system messages. If omitted
|
|
||||||
info/system messages as well as normal messages will be matched.
|
|
||||||
:param func: A Callable function that should accept the event as input
|
|
||||||
parameter, and return a bool value indicating whether the event
|
|
||||||
should be dispatched or not.
|
|
||||||
"""
|
|
||||||
super().__init__(func=func)
|
super().__init__(func=func)
|
||||||
self.is_bot = is_bot
|
self.is_bot = is_bot
|
||||||
self.is_info = is_info
|
self.is_info = is_info
|
||||||
@@ -162,7 +159,6 @@ class NewMessage(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
"""Return true if if the event is a new message event."""
|
|
||||||
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
|
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
|
||||||
return False
|
return False
|
||||||
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
|
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
|
||||||
@@ -203,7 +199,6 @@ class MemberListChanged(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
"""Return true if if the event is a member addition event."""
|
|
||||||
if self.added is not None and self.added != event.member_added:
|
if self.added is not None and self.added != event.member_added:
|
||||||
return False
|
return False
|
||||||
return self._call_func(event)
|
return self._call_func(event)
|
||||||
@@ -236,7 +231,6 @@ class GroupImageChanged(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
"""Return True if event is matched."""
|
|
||||||
if self.deleted is not None and self.deleted != event.image_deleted:
|
if self.deleted is not None and self.deleted != event.image_deleted:
|
||||||
return False
|
return False
|
||||||
return self._call_func(event)
|
return self._call_func(event)
|
||||||
@@ -262,12 +256,13 @@ class GroupNameChanged(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
"""Return True if event is matched."""
|
|
||||||
return self._call_func(event)
|
return self._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class HookCollection:
|
class HookCollection:
|
||||||
"""Helper class to collect event hooks that can later be added to a Delta Chat client."""
|
"""
|
||||||
|
Helper class to collect event hooks that can later be added to a Delta Chat client.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()
|
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"""Message module."""
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, List, Optional, Union
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
|
||||||
from ._utils import AttrDict, futuremethod
|
from ._utils import AttrDict, futuremethod
|
||||||
from .const import EventType
|
from .const import EventType
|
||||||
@@ -25,14 +23,7 @@ class Message:
|
|||||||
return self.account._rpc
|
return self.account._rpc
|
||||||
|
|
||||||
def send_reaction(self, *reaction: str) -> "Message":
|
def send_reaction(self, *reaction: str) -> "Message":
|
||||||
"""
|
"""Send a reaction to this message."""
|
||||||
Sends a reaction to message.
|
|
||||||
|
|
||||||
A reaction is a string that represents an emoji.
|
|
||||||
You can call this function again to change the emoji;
|
|
||||||
the last sent reaction overrides all previously sent reactions.
|
|
||||||
It is possible to remove the reaction by sending an empty string.
|
|
||||||
"""
|
|
||||||
msg_id = self._rpc.send_reaction(self.account.id, self.id, reaction)
|
msg_id = self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
@@ -46,19 +37,6 @@ class Message:
|
|||||||
snapshot["message"] = self
|
snapshot["message"] = self
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
def get_read_receipts(self) -> List[AttrDict]:
|
|
||||||
"""Get message read receipts."""
|
|
||||||
read_receipts = self._rpc.get_message_read_receipts(self.account.id, self.id)
|
|
||||||
return [AttrDict(read_receipt) for read_receipt in read_receipts]
|
|
||||||
|
|
||||||
def get_read_receipt_count(self) -> int:
|
|
||||||
"""
|
|
||||||
Returns count of read receipts on message.
|
|
||||||
|
|
||||||
This view count is meant as a feedback measure for the channel owner only.
|
|
||||||
"""
|
|
||||||
return self._rpc.get_message_read_receipt_count(self.account.id, self.id)
|
|
||||||
|
|
||||||
def get_reactions(self) -> Optional[AttrDict]:
|
def get_reactions(self) -> Optional[AttrDict]:
|
||||||
"""Get message reactions."""
|
"""Get message reactions."""
|
||||||
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
||||||
@@ -67,7 +45,6 @@ class Message:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_sender_contact(self) -> Contact:
|
def get_sender_contact(self) -> Contact:
|
||||||
"""Return sender contact."""
|
|
||||||
from_id = self.get_snapshot().from_id
|
from_id = self.get_snapshot().from_id
|
||||||
return self.account.get_contact_by_id(from_id)
|
return self.account.get_contact_by_id(from_id)
|
||||||
|
|
||||||
@@ -75,9 +52,8 @@ class Message:
|
|||||||
"""Mark the message as seen."""
|
"""Mark the message as seen."""
|
||||||
self._rpc.markseen_msgs(self.account.id, [self.id])
|
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||||
|
|
||||||
def exists(self) -> bool:
|
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
|
||||||
"""Return True if the message exists."""
|
self._rpc.continue_autocrypt_key_transfer(self.account.id, self.id, setup_code)
|
||||||
return bool(self._rpc.get_existing_msg_ids(self.account.id, [self.id]))
|
|
||||||
|
|
||||||
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||||
"""Send a webxdc status update. This message must be a webxdc."""
|
"""Send a webxdc status update. This message must be a webxdc."""
|
||||||
@@ -86,7 +62,6 @@ class Message:
|
|||||||
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
||||||
|
|
||||||
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
||||||
"""Return a list of Webxdc status updates for Webxdc instance message."""
|
|
||||||
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
||||||
|
|
||||||
def get_info(self) -> str:
|
def get_info(self) -> str:
|
||||||
@@ -94,7 +69,6 @@ class Message:
|
|||||||
return self._rpc.get_message_info(self.account.id, self.id)
|
return self._rpc.get_message_info(self.account.id, self.id)
|
||||||
|
|
||||||
def get_webxdc_info(self) -> dict:
|
def get_webxdc_info(self) -> dict:
|
||||||
"""Get info from a Webxdc message in JSON format."""
|
|
||||||
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||||
|
|
||||||
def wait_until_delivered(self) -> None:
|
def wait_until_delivered(self) -> None:
|
||||||
@@ -104,35 +78,10 @@ class Message:
|
|||||||
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
|
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
|
||||||
break
|
break
|
||||||
|
|
||||||
def resend(self) -> None:
|
|
||||||
"""Resend messages and make information available for newly added chat members.
|
|
||||||
Resending sends out the original message, however, recipients and webxdc-status may differ.
|
|
||||||
Clients that already have the original message can still ignore the resent message as
|
|
||||||
they have tracked the state by dedicated updates.
|
|
||||||
|
|
||||||
Some messages cannot be resent, eg. info-messages, drafts, already pending messages,
|
|
||||||
or messages that are not sent by SELF.
|
|
||||||
"""
|
|
||||||
self._rpc.resend_messages(self.account.id, [self.id])
|
|
||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
def send_webxdc_realtime_advertisement(self):
|
def send_webxdc_realtime_advertisement(self):
|
||||||
"""Send an advertisement to join the realtime channel."""
|
|
||||||
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) -> None:
|
def send_webxdc_realtime_data(self, data) -> None:
|
||||||
"""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))
|
||||||
|
|
||||||
def accept_incoming_call(self, accept_call_info):
|
|
||||||
"""Accepts an incoming call."""
|
|
||||||
self._rpc.accept_incoming_call(self.account.id, self.id, accept_call_info)
|
|
||||||
|
|
||||||
def end_call(self):
|
|
||||||
"""Ends incoming or outgoing call."""
|
|
||||||
self._rpc.end_call(self.account.id, self.id)
|
|
||||||
|
|
||||||
def get_call_info(self) -> AttrDict:
|
|
||||||
"""Return information about the call."""
|
|
||||||
return AttrDict(self._rpc.call_info(self.account.id, self.id))
|
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
"""Pytest plugin module."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import pathlib
|
|
||||||
import platform
|
|
||||||
import random
|
import random
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from typing import AsyncGenerator, Optional
|
from typing import AsyncGenerator, Optional
|
||||||
|
|
||||||
import execnet
|
|
||||||
import py
|
import py
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -19,62 +11,35 @@ from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Messag
|
|||||||
from ._utils import futuremethod
|
from ._utils import futuremethod
|
||||||
from .rpc import Rpc
|
from .rpc import Rpc
|
||||||
|
|
||||||
E2EE_INFO_MSGS = 1
|
|
||||||
"""
|
|
||||||
The number of info messages added to new e2ee chats.
|
|
||||||
Currently this is "Messages are end-to-end encrypted."
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_report_header():
|
|
||||||
for base in os.get_exec_path():
|
|
||||||
fn = pathlib.Path(base).joinpath(base, "deltachat-rpc-server")
|
|
||||||
if fn.exists():
|
|
||||||
proc = subprocess.Popen([str(fn), "--version"], stderr=subprocess.PIPE)
|
|
||||||
proc.wait()
|
|
||||||
version = proc.stderr.read().decode().strip()
|
|
||||||
return f"deltachat-rpc-server: {fn} [{version}]"
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class ACFactory:
|
class ACFactory:
|
||||||
"""Test account factory."""
|
|
||||||
|
|
||||||
def __init__(self, deltachat: DeltaChat) -> None:
|
def __init__(self, deltachat: DeltaChat) -> None:
|
||||||
self.deltachat = deltachat
|
self.deltachat = deltachat
|
||||||
|
|
||||||
def get_unconfigured_account(self) -> Account:
|
def get_unconfigured_account(self) -> Account:
|
||||||
"""Create a new unconfigured account."""
|
account = self.deltachat.add_account()
|
||||||
return self.deltachat.add_account()
|
account.set_config("verified_one_on_one_chats", "1")
|
||||||
|
return account
|
||||||
|
|
||||||
def get_unconfigured_bot(self) -> Bot:
|
def get_unconfigured_bot(self) -> Bot:
|
||||||
"""Create a new unconfigured bot."""
|
|
||||||
return Bot(self.get_unconfigured_account())
|
return Bot(self.get_unconfigured_account())
|
||||||
|
|
||||||
def get_credentials(self) -> (str, str):
|
def get_credentials(self) -> (str, str):
|
||||||
"""Generate new credentials for chatmail account."""
|
domain = os.getenv("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))
|
||||||
return f"{username}@{domain}", f"{username}${username}"
|
return f"{username}@{domain}", f"{username}${username}"
|
||||||
|
|
||||||
def get_account_qr(self):
|
|
||||||
"""Return "dcaccount:" QR code for testing chatmail relay."""
|
|
||||||
domain = os.environ["CHATMAIL_DOMAIN"]
|
|
||||||
return f"dcaccount:{domain}"
|
|
||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
def new_configured_account(self):
|
def new_configured_account(self):
|
||||||
"""Create a new configured account."""
|
addr, password = self.get_credentials()
|
||||||
account = self.get_unconfigured_account()
|
account = self.get_unconfigured_account()
|
||||||
qr = self.get_account_qr()
|
params = {"addr": addr, "password": password}
|
||||||
yield account.add_transport_from_qr.future(qr)
|
yield account.add_or_update_transport.future(params)
|
||||||
|
|
||||||
assert account.is_configured()
|
assert account.is_configured()
|
||||||
return account
|
return account
|
||||||
|
|
||||||
def new_configured_bot(self) -> Bot:
|
def new_configured_bot(self) -> Bot:
|
||||||
"""Create a new configured bot."""
|
|
||||||
addr, password = self.get_credentials()
|
addr, password = self.get_credentials()
|
||||||
bot = self.get_unconfigured_bot()
|
bot = self.get_unconfigured_bot()
|
||||||
bot.configure(addr, password)
|
bot.configure(addr, password)
|
||||||
@@ -82,32 +47,25 @@ class ACFactory:
|
|||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
def get_online_account(self):
|
def get_online_account(self):
|
||||||
"""Create a new account and start I/O."""
|
|
||||||
account = yield self.new_configured_account.future()
|
account = yield self.new_configured_account.future()
|
||||||
account.bring_online()
|
account.bring_online()
|
||||||
return account
|
return account
|
||||||
|
|
||||||
def get_online_accounts(self, num: int) -> list[Account]:
|
def get_online_accounts(self, num: int) -> list[Account]:
|
||||||
"""Create multiple online accounts."""
|
|
||||||
futures = [self.get_online_account.future() for _ in range(num)]
|
futures = [self.get_online_account.future() for _ in range(num)]
|
||||||
return [f() for f in futures]
|
return [f() for f in futures]
|
||||||
|
|
||||||
def resetup_account(self, ac: Account) -> Account:
|
def resetup_account(self, ac: Account) -> Account:
|
||||||
"""Resetup account from scratch, losing the encryption key."""
|
"""Resetup account from scratch, losing the encryption key."""
|
||||||
ac.stop_io()
|
ac.stop_io()
|
||||||
transports = ac.list_transports()
|
|
||||||
ac.remove()
|
|
||||||
ac_clone = self.get_unconfigured_account()
|
ac_clone = self.get_unconfigured_account()
|
||||||
for transport in transports:
|
for i in ["addr", "mail_pw"]:
|
||||||
ac_clone.add_or_update_transport(transport)
|
ac_clone.set_config(i, ac.get_config(i))
|
||||||
ac_clone.bring_online()
|
ac.remove()
|
||||||
|
ac_clone.configure()
|
||||||
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 1:1 chat between ac1 and ac2 accepted on both sides.
|
|
||||||
|
|
||||||
Returned chat is a chat with ac2 from ac1 point of view.
|
|
||||||
"""
|
|
||||||
ac2.create_chat(ac1)
|
ac2.create_chat(ac1)
|
||||||
return ac1.create_chat(ac2)
|
return ac1.create_chat(ac2)
|
||||||
|
|
||||||
@@ -119,7 +77,6 @@ class ACFactory:
|
|||||||
file: Optional[str] = None,
|
file: Optional[str] = None,
|
||||||
group: Optional[str] = None,
|
group: Optional[str] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""Send a message."""
|
|
||||||
if not from_account:
|
if not from_account:
|
||||||
from_account = (self.get_online_accounts(1))[0]
|
from_account = (self.get_online_accounts(1))[0]
|
||||||
to_contact = from_account.create_contact(to_account)
|
to_contact = from_account.create_contact(to_account)
|
||||||
@@ -138,7 +95,6 @@ class ACFactory:
|
|||||||
file: Optional[str] = None,
|
file: Optional[str] = None,
|
||||||
group: Optional[str] = None,
|
group: Optional[str] = None,
|
||||||
) -> AttrDict:
|
) -> AttrDict:
|
||||||
"""Send a message and wait until recipient processes it."""
|
|
||||||
self.send_message(
|
self.send_message(
|
||||||
to_account=to_client.account,
|
to_account=to_client.account,
|
||||||
from_account=from_account,
|
from_account=from_account,
|
||||||
@@ -152,22 +108,14 @@ class ACFactory:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def rpc(tmp_path) -> AsyncGenerator:
|
def rpc(tmp_path) -> AsyncGenerator:
|
||||||
"""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:
|
||||||
yield rpc_server
|
yield rpc_server
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def dc(rpc) -> DeltaChat:
|
def acfactory(rpc) -> AsyncGenerator:
|
||||||
"""Return account manager."""
|
return ACFactory(DeltaChat(rpc))
|
||||||
return DeltaChat(rpc)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def acfactory(dc) -> AsyncGenerator:
|
|
||||||
"""Return account factory fixture."""
|
|
||||||
return ACFactory(dc)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -184,7 +132,7 @@ def data():
|
|||||||
raise Exception("Data path cannot be found")
|
raise Exception("Data path cannot be found")
|
||||||
|
|
||||||
def get_path(self, bn):
|
def get_path(self, bn):
|
||||||
"""Return path of file or None if it doesn't exist."""
|
"""return path of file or None if it doesn't exist."""
|
||||||
fn = os.path.join(self.path, *bn.split("/"))
|
fn = os.path.join(self.path, *bn.split("/"))
|
||||||
assert os.path.exists(fn)
|
assert os.path.exists(fn)
|
||||||
return fn
|
return fn
|
||||||
@@ -205,143 +153,13 @@ def log():
|
|||||||
|
|
||||||
class Printer:
|
class Printer:
|
||||||
def section(self, msg: str) -> None:
|
def section(self, msg: str) -> None:
|
||||||
logging.info("\n%s %s %s", "=" * 10, msg, "=" * 10)
|
print()
|
||||||
|
print("=" * 10, msg, "=" * 10)
|
||||||
|
|
||||||
def step(self, msg: str) -> None:
|
def step(self, msg: str) -> None:
|
||||||
logging.info("%s step %s %s", "-" * 5, msg, "-" * 5)
|
print("-" * 5, "step " + msg, "-" * 5)
|
||||||
|
|
||||||
def indent(self, msg: str) -> None:
|
def indent(self, msg: str) -> None:
|
||||||
logging.info(" " + msg)
|
print(" " + msg)
|
||||||
|
|
||||||
return Printer()
|
return Printer()
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# support for testing against different deltachat-rpc-server/clients
|
|
||||||
# installed into a temporary virtualenv and connected via 'execnet' channels
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
def find_path(venv, name):
|
|
||||||
is_windows = platform.system() == "Windows"
|
|
||||||
bin = venv / ("bin" if not is_windows else "Scripts")
|
|
||||||
|
|
||||||
tryadd = [""]
|
|
||||||
if is_windows:
|
|
||||||
tryadd += os.environ["PATHEXT"].split(os.pathsep)
|
|
||||||
for ext in tryadd:
|
|
||||||
p = bin.joinpath(name + ext)
|
|
||||||
if p.exists():
|
|
||||||
return str(p)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def get_core_python_env(tmp_path_factory):
|
|
||||||
"""Return a factory to create virtualenv environments with rpc server/client packages
|
|
||||||
installed.
|
|
||||||
|
|
||||||
The factory takes a version and returns a (python_path, rpc_server_path) tuple
|
|
||||||
of the respective binaries in the virtualenv.
|
|
||||||
"""
|
|
||||||
|
|
||||||
envs = {}
|
|
||||||
|
|
||||||
def get_versioned_venv(core_version):
|
|
||||||
venv = envs.get(core_version)
|
|
||||||
if not venv:
|
|
||||||
venv = tmp_path_factory.mktemp(f"temp-{core_version}")
|
|
||||||
subprocess.check_call([sys.executable, "-m", "venv", venv])
|
|
||||||
|
|
||||||
python = find_path(venv, "python")
|
|
||||||
pkgs = [f"deltachat-rpc-server=={core_version}", f"deltachat-rpc-client=={core_version}", "pytest"]
|
|
||||||
subprocess.check_call([python, "-m", "pip", "install"] + pkgs)
|
|
||||||
|
|
||||||
envs[core_version] = venv
|
|
||||||
python = find_path(venv, "python")
|
|
||||||
rpc_server_path = find_path(venv, "deltachat-rpc-server")
|
|
||||||
logging.info(f"Paths:\npython={python}\nrpc_server={rpc_server_path}")
|
|
||||||
return python, rpc_server_path
|
|
||||||
|
|
||||||
return get_versioned_venv
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
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.
|
|
||||||
|
|
||||||
The 'eval' function allows to remote-execute arbitrary expressions
|
|
||||||
that can use the `bob` online account, and the `bob_contact_alice`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def factory(core_version):
|
|
||||||
python, rpc_server_path = get_core_python_env(core_version)
|
|
||||||
gw = execnet.makegateway(f"popen//python={python}")
|
|
||||||
|
|
||||||
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
|
|
||||||
channel = gw.remote_exec(remote_bob_loop)
|
|
||||||
cm = os.environ.get("CHATMAIL_DOMAIN")
|
|
||||||
|
|
||||||
# trigger getting an online account on bob's side
|
|
||||||
channel.send((accounts_dir, str(rpc_server_path), cm))
|
|
||||||
|
|
||||||
# meanwhile get a local alice account
|
|
||||||
alice = acfactory.get_online_account()
|
|
||||||
channel.send(alice.self_contact.make_vcard())
|
|
||||||
|
|
||||||
# wait for bob to have started
|
|
||||||
sysinfo = channel.receive()
|
|
||||||
assert sysinfo == f"v{core_version}"
|
|
||||||
bob_vcard = channel.receive()
|
|
||||||
[alice_contact_bob] = alice.import_vcard(bob_vcard)
|
|
||||||
|
|
||||||
def eval(eval_str):
|
|
||||||
channel.send(eval_str)
|
|
||||||
return channel.receive()
|
|
||||||
|
|
||||||
return alice, alice_contact_bob, eval
|
|
||||||
|
|
||||||
return factory
|
|
||||||
|
|
||||||
|
|
||||||
def remote_bob_loop(channel):
|
|
||||||
# This function executes with versioned
|
|
||||||
# deltachat-rpc-client/server packages
|
|
||||||
# installed into the virtualenv.
|
|
||||||
#
|
|
||||||
# The "channel" argument is a send/receive pipe
|
|
||||||
# to the process that runs the corresponding remote_exec(remote_bob_loop)
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from deltachat_rpc_client import DeltaChat, Rpc
|
|
||||||
from deltachat_rpc_client.pytestplugin import ACFactory
|
|
||||||
|
|
||||||
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
|
|
||||||
# so we can't just pass `rpc_server_path` argument to Rpc constructor
|
|
||||||
basepath = os.path.dirname(rpc_server_path)
|
|
||||||
os.environ["PATH"] = os.pathsep.join([basepath, os.environ["PATH"]])
|
|
||||||
rpc = Rpc(accounts_dir=accounts_dir)
|
|
||||||
|
|
||||||
with rpc:
|
|
||||||
dc = DeltaChat(rpc)
|
|
||||||
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
|
|
||||||
acfactory = ACFactory(dc)
|
|
||||||
bob = acfactory.get_online_account()
|
|
||||||
alice_vcard = channel.receive()
|
|
||||||
[alice_contact] = bob.import_vcard(alice_vcard)
|
|
||||||
ns = {"bob": bob, "bob_contact_alice": alice_contact}
|
|
||||||
channel.send(bob.self_contact.make_vcard())
|
|
||||||
|
|
||||||
while 1:
|
|
||||||
eval_str = channel.receive()
|
|
||||||
res = eval(eval_str, ns)
|
|
||||||
try:
|
|
||||||
channel.send(res)
|
|
||||||
except Exception:
|
|
||||||
# some unserializable result
|
|
||||||
channel.send(None)
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"""JSON-RPC client module."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
@@ -9,28 +7,42 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from queue import Empty, Queue
|
from queue import Empty, Queue
|
||||||
from threading import Thread
|
from threading import Event, Thread
|
||||||
from typing import Any, Iterator, Optional
|
from typing import Any, Iterator, Optional
|
||||||
|
|
||||||
|
|
||||||
class JsonRpcError(Exception):
|
class JsonRpcError(Exception):
|
||||||
"""JSON-RPC error."""
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RpcFuture:
|
||||||
|
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
|
||||||
|
self.rpc = rpc
|
||||||
|
self.request_id = request_id
|
||||||
|
self.event = event
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
self.event.wait()
|
||||||
|
response = self.rpc.request_results.pop(self.request_id)
|
||||||
|
if "error" in response:
|
||||||
|
raise JsonRpcError(response["error"])
|
||||||
|
if "result" in response:
|
||||||
|
return response["result"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class RpcMethod:
|
class RpcMethod:
|
||||||
"""RPC method."""
|
|
||||||
|
|
||||||
def __init__(self, rpc: "Rpc", name: str):
|
def __init__(self, rpc: "Rpc", name: str):
|
||||||
self.rpc = rpc
|
self.rpc = rpc
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
def __call__(self, *args) -> Any:
|
def __call__(self, *args) -> Any:
|
||||||
"""Call JSON-RPC method synchronously."""
|
"""Synchronously calls JSON-RPC method."""
|
||||||
future = self.future(*args)
|
future = self.future(*args)
|
||||||
return future()
|
return future()
|
||||||
|
|
||||||
def future(self, *args) -> Any:
|
def future(self, *args) -> Any:
|
||||||
"""Call JSON-RPC method asynchronously."""
|
"""Asynchronously calls JSON-RPC method."""
|
||||||
request_id = next(self.rpc.id_iterator)
|
request_id = next(self.rpc.id_iterator)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -38,32 +50,16 @@ class RpcMethod:
|
|||||||
"params": args,
|
"params": args,
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
}
|
}
|
||||||
self.rpc.request_results[request_id] = queue = Queue()
|
event = Event()
|
||||||
|
self.rpc.request_events[request_id] = event
|
||||||
self.rpc.request_queue.put(request)
|
self.rpc.request_queue.put(request)
|
||||||
|
|
||||||
def rpc_future():
|
return RpcFuture(self.rpc, request_id, event)
|
||||||
"""Wait for the request to receive a result."""
|
|
||||||
response = queue.get()
|
|
||||||
if "error" in response:
|
|
||||||
raise JsonRpcError(response["error"])
|
|
||||||
return response.get("result", None)
|
|
||||||
|
|
||||||
return rpc_future
|
|
||||||
|
|
||||||
|
|
||||||
class Rpc:
|
class Rpc:
|
||||||
"""RPC client."""
|
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
||||||
|
"""The given arguments will be passed to subprocess.Popen()"""
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
accounts_dir: Optional[str] = None,
|
|
||||||
rpc_server_path="deltachat-rpc-server",
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""Initialize RPC client.
|
|
||||||
|
|
||||||
The 'kwargs' arguments will be passed to subprocess.Popen().
|
|
||||||
"""
|
|
||||||
if accounts_dir:
|
if accounts_dir:
|
||||||
kwargs["env"] = {
|
kwargs["env"] = {
|
||||||
**kwargs.get("env", os.environ),
|
**kwargs.get("env", os.environ),
|
||||||
@@ -71,12 +67,13 @@ class Rpc:
|
|||||||
}
|
}
|
||||||
|
|
||||||
self._kwargs = kwargs
|
self._kwargs = kwargs
|
||||||
self.rpc_server_path = rpc_server_path
|
|
||||||
self.process: subprocess.Popen
|
self.process: subprocess.Popen
|
||||||
self.id_iterator: Iterator[int]
|
self.id_iterator: Iterator[int]
|
||||||
self.event_queues: dict[int, Queue]
|
self.event_queues: dict[int, Queue]
|
||||||
# Map from request ID to a Queue which provides a single result
|
# Map from request ID to `threading.Event`.
|
||||||
self.request_results: dict[int, Queue]
|
self.request_events: dict[int, Event]
|
||||||
|
# Map from request ID to the result.
|
||||||
|
self.request_results: dict[int, Any]
|
||||||
self.request_queue: Queue[Any]
|
self.request_queue: Queue[Any]
|
||||||
self.closing: bool
|
self.closing: bool
|
||||||
self.reader_thread: Thread
|
self.reader_thread: Thread
|
||||||
@@ -84,27 +81,27 @@ class Rpc:
|
|||||||
self.events_thread: Thread
|
self.events_thread: Thread
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
"""Start RPC server subprocess and wait for successful initialization.
|
|
||||||
|
|
||||||
This method blocks until the RPC server responds to an initial
|
|
||||||
health-check RPC call (get_system_info).
|
|
||||||
If the server fails to start
|
|
||||||
(e.g., due to an invalid accounts directory),
|
|
||||||
a JsonRpcError is raised.
|
|
||||||
"""
|
|
||||||
popen_kwargs = {"stdin": subprocess.PIPE, "stdout": subprocess.PIPE, "stderr": subprocess.PIPE}
|
|
||||||
if sys.version_info >= (3, 11):
|
if sys.version_info >= (3, 11):
|
||||||
# Prevent subprocess from capturing SIGINT.
|
self.process = subprocess.Popen(
|
||||||
popen_kwargs["process_group"] = 0
|
"deltachat-rpc-server",
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
# Prevent subprocess from capturing SIGINT.
|
||||||
|
process_group=0,
|
||||||
|
**self._kwargs,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# `process_group` is not supported before Python 3.11.
|
self.process = subprocess.Popen(
|
||||||
popen_kwargs["preexec_fn"] = os.setpgrp # noqa: PLW1509
|
"deltachat-rpc-server",
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
popen_kwargs.update(self._kwargs)
|
stdout=subprocess.PIPE,
|
||||||
self.process = subprocess.Popen(self.rpc_server_path, **popen_kwargs)
|
# `process_group` is not supported before Python 3.11.
|
||||||
|
preexec_fn=os.setpgrp, # noqa: PLW1509
|
||||||
|
**self._kwargs,
|
||||||
|
)
|
||||||
self.id_iterator = itertools.count(start=1)
|
self.id_iterator = itertools.count(start=1)
|
||||||
self.event_queues = {}
|
self.event_queues = {}
|
||||||
|
self.request_events = {}
|
||||||
self.request_results = {}
|
self.request_results = {}
|
||||||
self.request_queue = Queue()
|
self.request_queue = Queue()
|
||||||
self.closing = False
|
self.closing = False
|
||||||
@@ -115,22 +112,6 @@ class Rpc:
|
|||||||
self.events_thread = Thread(target=self.events_loop)
|
self.events_thread = Thread(target=self.events_loop)
|
||||||
self.events_thread.start()
|
self.events_thread.start()
|
||||||
|
|
||||||
# Perform a health-check RPC call to ensure the server started
|
|
||||||
# successfully and the accounts directory is usable.
|
|
||||||
try:
|
|
||||||
system_info = self.get_system_info()
|
|
||||||
except (JsonRpcError, Exception) as e:
|
|
||||||
# The reader_loop already saw EOF on stdout, so the process
|
|
||||||
# has exited and stderr is available.
|
|
||||||
stderr = self.process.stderr.read().decode(errors="replace").strip()
|
|
||||||
if stderr:
|
|
||||||
raise JsonRpcError(f"RPC server failed to start: {stderr}") from e
|
|
||||||
raise JsonRpcError(f"RPC server startup check failed: {e}") from e
|
|
||||||
logging.info(
|
|
||||||
"RPC server ready. Core version: %s",
|
|
||||||
system_info.get("deltachat_core_version", "unknown"),
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
||||||
@@ -149,22 +130,19 @@ class Rpc:
|
|||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def reader_loop(self) -> None:
|
def reader_loop(self) -> None:
|
||||||
"""Process JSON-RPC responses from the RPC server process output."""
|
|
||||||
try:
|
try:
|
||||||
while line := self.process.stdout.readline():
|
while line := self.process.stdout.readline():
|
||||||
response = json.loads(line)
|
response = json.loads(line)
|
||||||
if "id" in response:
|
if "id" in response:
|
||||||
response_id = response["id"]
|
response_id = response["id"]
|
||||||
self.request_results.pop(response_id).put(response)
|
event = self.request_events.pop(response_id)
|
||||||
|
self.request_results[response_id] = response
|
||||||
|
event.set()
|
||||||
else:
|
else:
|
||||||
logging.warning("Got a response without ID: %s", response)
|
logging.warning("Got a response without ID: %s", response)
|
||||||
except Exception:
|
except Exception:
|
||||||
# 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:
|
|
||||||
# Unblock any pending requests when the server closes stdout.
|
|
||||||
for _request_id, queue in self.request_results.items():
|
|
||||||
queue.put({"error": {"code": -32000, "message": "RPC server closed"}})
|
|
||||||
|
|
||||||
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."""
|
||||||
@@ -173,39 +151,39 @@ class Rpc:
|
|||||||
data = (json.dumps(request) + "\n").encode()
|
data = (json.dumps(request) + "\n").encode()
|
||||||
self.process.stdin.write(data)
|
self.process.stdin.write(data)
|
||||||
self.process.stdin.flush()
|
self.process.stdin.flush()
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# Log an exception if the writer loop dies.
|
# Log an exception if the writer loop dies.
|
||||||
logging.exception("Exception in the writer loop")
|
logging.exception("Exception in the writer loop")
|
||||||
|
|
||||||
def get_queue(self, account_id: int) -> Queue:
|
def get_queue(self, account_id: int) -> Queue:
|
||||||
"""Get event queue corresponding to the given account ID."""
|
|
||||||
if account_id not in self.event_queues:
|
if account_id not in self.event_queues:
|
||||||
self.event_queues[account_id] = Queue()
|
self.event_queues[account_id] = Queue()
|
||||||
return self.event_queues[account_id]
|
return self.event_queues[account_id]
|
||||||
|
|
||||||
def events_loop(self) -> None:
|
def events_loop(self) -> None:
|
||||||
"""Request new events and distributes them between queues."""
|
"""Requests new events and distributes them between queues."""
|
||||||
try:
|
try:
|
||||||
while events := self.get_next_event_batch():
|
while True:
|
||||||
for event in events:
|
|
||||||
account_id = event["contextId"]
|
|
||||||
queue = self.get_queue(account_id)
|
|
||||||
payload = event["event"]
|
|
||||||
logging.debug("account_id=%d got an event %s", account_id, payload)
|
|
||||||
queue.put(payload)
|
|
||||||
if self.closing:
|
if self.closing:
|
||||||
return
|
return
|
||||||
|
event = self.get_next_event()
|
||||||
|
account_id = event["contextId"]
|
||||||
|
queue = self.get_queue(account_id)
|
||||||
|
event = event["event"]
|
||||||
|
logging.debug("account_id=%d got an event %s", account_id, event)
|
||||||
|
queue.put(event)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Log an exception if the event loop dies.
|
# Log an exception if the event loop dies.
|
||||||
logging.exception("Exception in the event loop")
|
logging.exception("Exception in the event loop")
|
||||||
|
|
||||||
def wait_for_event(self, account_id: int) -> Optional[dict]:
|
def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||||
"""Wait for the next event from the given account and returns it."""
|
"""Waits for the next event from the given account and returns it."""
|
||||||
queue = self.get_queue(account_id)
|
queue = self.get_queue(account_id)
|
||||||
return queue.get()
|
return queue.get()
|
||||||
|
|
||||||
def clear_all_events(self, account_id: int):
|
def clear_all_events(self, account_id: int):
|
||||||
"""Remove all queued-up events for a given account. Useful for tests."""
|
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||||
queue = self.get_queue(account_id)
|
queue = self.get_queue(account_id)
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import imaplib
|
import imaplib
|
||||||
import io
|
import io
|
||||||
import logging
|
|
||||||
import pathlib
|
import pathlib
|
||||||
import ssl
|
import ssl
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
@@ -46,13 +45,13 @@ class DirectImap:
|
|||||||
try:
|
try:
|
||||||
self.conn.logout()
|
self.conn.logout()
|
||||||
except (OSError, imaplib.IMAP4.abort):
|
except (OSError, imaplib.IMAP4.abort):
|
||||||
logging.warning("Could not logout direct_imap conn")
|
print("Could not logout direct_imap conn")
|
||||||
|
|
||||||
def create_folder(self, foldername):
|
def create_folder(self, foldername):
|
||||||
try:
|
try:
|
||||||
self.conn.folder.create(foldername)
|
self.conn.folder.create(foldername)
|
||||||
except errors.MailboxFolderCreateError as e:
|
except errors.MailboxFolderCreateError as e:
|
||||||
logging.warning(f"Cannot create '{foldername}', probably it already exists: {str(e)}")
|
print("Can't create", foldername, "probably it already exists:", str(e))
|
||||||
|
|
||||||
def select_folder(self, foldername: str) -> tuple:
|
def select_folder(self, foldername: str) -> tuple:
|
||||||
assert not self._idling
|
assert not self._idling
|
||||||
@@ -86,17 +85,17 @@ class DirectImap:
|
|||||||
|
|
||||||
def get_all_messages(self) -> list[MailMessage]:
|
def get_all_messages(self) -> list[MailMessage]:
|
||||||
assert not self._idling
|
assert not self._idling
|
||||||
return list(self.conn.fetch(mark_seen=False))
|
return list(self.conn.fetch())
|
||||||
|
|
||||||
def get_unread_messages(self) -> list[str]:
|
def get_unread_messages(self) -> list[str]:
|
||||||
assert not self._idling
|
assert not self._idling
|
||||||
return [msg.uid for msg in self.conn.fetch(AND(seen=False), mark_seen=False)]
|
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
|
||||||
|
|
||||||
def mark_all_read(self):
|
def mark_all_read(self):
|
||||||
messages = self.get_unread_messages()
|
messages = self.get_unread_messages()
|
||||||
if messages:
|
if messages:
|
||||||
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
|
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
|
||||||
logging.info(f"Marked seen: {messages} {res}")
|
print("marked seen:", messages, res)
|
||||||
|
|
||||||
def get_unread_cnt(self) -> int:
|
def get_unread_cnt(self) -> int:
|
||||||
return len(self.get_unread_messages())
|
return len(self.get_unread_messages())
|
||||||
@@ -174,6 +173,7 @@ 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
|
||||||
|
self.log = direct_imap.account.log
|
||||||
# fetch latest messages before starting idle so that it only
|
# fetch latest messages before starting idle so that it only
|
||||||
# returns messages that arrive anew
|
# returns messages that arrive anew
|
||||||
self.direct_imap.conn.fetch("1:*")
|
self.direct_imap.conn.fetch("1:*")
|
||||||
@@ -181,11 +181,14 @@ class IdleManager:
|
|||||||
|
|
||||||
def check(self, timeout=None) -> list[bytes]:
|
def check(self, timeout=None) -> list[bytes]:
|
||||||
"""(blocking) wait for next idle message from server."""
|
"""(blocking) wait for next idle message from server."""
|
||||||
return self.direct_imap.conn.idle.poll(timeout=timeout)
|
self.log("imap-direct: calling idle_check")
|
||||||
|
res = self.direct_imap.conn.idle.poll(timeout=timeout)
|
||||||
|
self.log(f"imap-direct: idle_check returned {res!r}")
|
||||||
|
return res
|
||||||
|
|
||||||
def wait_for_new_message(self) -> bytes:
|
def wait_for_new_message(self, timeout=None) -> bytes:
|
||||||
while True:
|
while True:
|
||||||
for item in self.check():
|
for item in self.check(timeout=timeout):
|
||||||
if b"EXISTS" in item or b"RECENT" in item:
|
if b"EXISTS" in item or b"RECENT" in item:
|
||||||
return item
|
return item
|
||||||
|
|
||||||
@@ -193,8 +196,10 @@ class IdleManager:
|
|||||||
"""Return first message with SEEN flag from a running idle-stream."""
|
"""Return first message with SEEN flag from a running idle-stream."""
|
||||||
while True:
|
while True:
|
||||||
for item in self.check(timeout=timeout):
|
for item in self.check(timeout=timeout):
|
||||||
if FETCH in item and FLAGS in item and rb"\Seen" in item:
|
if FETCH in item:
|
||||||
return int(item.split(b" ")[1])
|
self.log(str(item))
|
||||||
|
if FLAGS in item and rb"\Seen" in item:
|
||||||
|
return int(item.split(b" ")[1])
|
||||||
|
|
||||||
def done(self):
|
def done(self):
|
||||||
"""send idle-done to server if we are currently in idle mode."""
|
"""send idle-done to server if we are currently in idle mode."""
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
from deltachat_rpc_client import EventType, Message
|
|
||||||
|
|
||||||
|
|
||||||
def test_calls(acfactory) -> None:
|
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
place_call_info = "offer"
|
|
||||||
accept_call_info = "answer"
|
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
|
||||||
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
|
||||||
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info, has_video_initially=True)
|
|
||||||
assert outgoing_call_message.get_call_info().state.kind == "Alerting"
|
|
||||||
|
|
||||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
|
||||||
assert incoming_call_event.place_call_info == place_call_info
|
|
||||||
assert incoming_call_event.has_video
|
|
||||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
|
||||||
assert incoming_call_message.get_call_info().state.kind == "Alerting"
|
|
||||||
assert incoming_call_message.get_call_info().has_video
|
|
||||||
|
|
||||||
incoming_call_message.accept_incoming_call(accept_call_info)
|
|
||||||
assert incoming_call_message.get_call_info().sdp_offer == place_call_info
|
|
||||||
assert incoming_call_message.get_call_info().state.kind == "Active"
|
|
||||||
outgoing_call_accepted_event = alice.wait_for_event(EventType.OUTGOING_CALL_ACCEPTED)
|
|
||||||
assert outgoing_call_accepted_event.accept_call_info == accept_call_info
|
|
||||||
assert outgoing_call_message.get_call_info().state.kind == "Active"
|
|
||||||
|
|
||||||
outgoing_call_message.end_call()
|
|
||||||
assert outgoing_call_message.get_call_info().state.kind == "Completed"
|
|
||||||
|
|
||||||
end_call_event = bob.wait_for_event(EventType.CALL_ENDED)
|
|
||||||
assert end_call_event.msg_id == outgoing_call_message.id
|
|
||||||
assert incoming_call_message.get_call_info().state.kind == "Completed"
|
|
||||||
|
|
||||||
|
|
||||||
def test_video_call(acfactory) -> None:
|
|
||||||
# Example from <https://datatracker.ietf.org/doc/rfc9143/>
|
|
||||||
# with `s= ` replaced with `s=-`.
|
|
||||||
#
|
|
||||||
# `s=` cannot be empty according to RFC 3264,
|
|
||||||
# so it is more clear as `s=-`.
|
|
||||||
|
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
|
||||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
|
||||||
|
|
||||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
|
||||||
assert incoming_call_event.place_call_info == "offer"
|
|
||||||
assert incoming_call_event.has_video
|
|
||||||
|
|
||||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
|
||||||
assert incoming_call_message.get_call_info().has_video
|
|
||||||
|
|
||||||
|
|
||||||
def test_audio_call(acfactory) -> None:
|
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
|
||||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=False)
|
|
||||||
|
|
||||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
|
||||||
assert incoming_call_event.place_call_info == "offer"
|
|
||||||
assert not incoming_call_event.has_video
|
|
||||||
|
|
||||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
|
||||||
assert not incoming_call_message.get_call_info().has_video
|
|
||||||
|
|
||||||
|
|
||||||
def test_ice_servers(acfactory) -> None:
|
|
||||||
alice = acfactory.get_online_account()
|
|
||||||
|
|
||||||
ice_servers = alice.ice_servers()
|
|
||||||
assert len(ice_servers) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_contact_request_call(acfactory) -> None:
|
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
|
||||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
|
||||||
alice_chat_bob.send_text("Hello!")
|
|
||||||
|
|
||||||
# Notification for "Hello!" message should arrive
|
|
||||||
# without the call ringing.
|
|
||||||
while True:
|
|
||||||
event = bob.wait_for_event()
|
|
||||||
|
|
||||||
# There should be no incoming call notification.
|
|
||||||
assert event.kind != EventType.INCOMING_CALL
|
|
||||||
|
|
||||||
if event.kind == EventType.MSGS_CHANGED:
|
|
||||||
msg = bob.get_message_by_id(event.msg_id)
|
|
||||||
if msg.get_snapshot().text == "Hello!":
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def test_who_can_call_me_nobody(acfactory) -> None:
|
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
# Bob sets "who can call me" to "nobody" (2)
|
|
||||||
bob.set_config("who_can_call_me", "2")
|
|
||||||
|
|
||||||
# Bob even accepts Alice in advance so the chat does not appear as contact request.
|
|
||||||
bob.create_chat(alice)
|
|
||||||
|
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
|
||||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
|
||||||
alice_chat_bob.send_text("Hello!")
|
|
||||||
|
|
||||||
# Notification for "Hello!" message should arrive
|
|
||||||
# without the call ringing.
|
|
||||||
while True:
|
|
||||||
event = bob.wait_for_event()
|
|
||||||
|
|
||||||
# There should be no incoming call notification.
|
|
||||||
assert event.kind != EventType.INCOMING_CALL
|
|
||||||
|
|
||||||
if event.kind == EventType.INCOMING_MSG:
|
|
||||||
msg = bob.get_message_by_id(event.msg_id)
|
|
||||||
if msg.get_snapshot().text == "Hello!":
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
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."""
|
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
# Bob sets "who can call me" to "nobody" (0)
|
|
||||||
bob.set_config("who_can_call_me", "0")
|
|
||||||
|
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
|
||||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
|
||||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
|
||||||
|
|
||||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
|
||||||
|
|
||||||
# Even with the call arriving, the chat is still in the contact request mode.
|
|
||||||
incoming_chat = incoming_call_message.get_snapshot().chat
|
|
||||||
assert incoming_chat.get_basic_snapshot().is_contact_request
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from deltachat_rpc_client import Account, EventType, const
|
from deltachat_rpc_client import Account, EventType, const
|
||||||
@@ -127,7 +129,7 @@ def test_download_on_demand(acfactory: ACFactory) -> 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="../test-data/image/screenshot.jpg",
|
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
|
||||||
)
|
)
|
||||||
|
|
||||||
message = alice.wait_for_incoming_msg()
|
message = alice.wait_for_incoming_msg()
|
||||||
@@ -167,8 +169,6 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
|||||||
"""
|
"""
|
||||||
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||||
|
|
||||||
bob.create_chat(alice)
|
|
||||||
|
|
||||||
alice_chat_bob.send_text("hello")
|
alice_chat_bob.send_text("hello")
|
||||||
|
|
||||||
msg = bob.wait_for_incoming_msg()
|
msg = bob.wait_for_incoming_msg()
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from deltachat_rpc_client import DeltaChat, Rpc
|
|
||||||
|
|
||||||
|
|
||||||
def test_install_venv_and_use_other_core(tmp_path, get_core_python_env):
|
|
||||||
python, rpc_server_path = get_core_python_env("2.24.0")
|
|
||||||
subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.24.0"])
|
|
||||||
rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=rpc_server_path)
|
|
||||||
|
|
||||||
with rpc:
|
|
||||||
dc = DeltaChat(rpc)
|
|
||||||
assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.24.0"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("version", ["2.24.0"])
|
|
||||||
def test_qr_setup_contact(alice_and_remote_bob, version) -> None:
|
|
||||||
"""Test other-core Bob profile can do securejoin with Alice on current core."""
|
|
||||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
|
|
||||||
|
|
||||||
qr_code = alice.get_qr_code()
|
|
||||||
remote_eval(f"bob.secure_join({qr_code!r})")
|
|
||||||
alice.wait_for_securejoin_inviter_success()
|
|
||||||
|
|
||||||
# Test that Alice verified Bob's profile.
|
|
||||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
|
||||||
assert alice_contact_bob_snapshot.is_verified
|
|
||||||
|
|
||||||
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
|
||||||
|
|
||||||
# Test that Bob verified Alice's profile.
|
|
||||||
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
|
|
||||||
|
|
||||||
|
|
||||||
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."""
|
|
||||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
|
|
||||||
|
|
||||||
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
|
|
||||||
|
|
||||||
msg = alice.wait_for_incoming_msg()
|
|
||||||
assert msg.get_snapshot().text == "hello"
|
|
||||||
|
|
||||||
|
|
||||||
def test_second_device(acfactory, alice_and_remote_bob) -> None:
|
|
||||||
"""Test setting up current version as a second device for old version."""
|
|
||||||
_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))")
|
|
||||||
qr = remote_eval("bob._rpc.get_backup_qr(bob.id)")
|
|
||||||
new_account = acfactory.get_unconfigured_account()
|
|
||||||
new_account._rpc.get_backup(new_account.id, qr)
|
|
||||||
remote_eval("locals()['future']()")
|
|
||||||
|
|
||||||
assert new_account.get_config("addr") == remote_eval("bob.get_config('addr')")
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
from imap_tools import AND, U
|
|
||||||
|
|
||||||
from deltachat_rpc_client import EventType
|
|
||||||
|
|
||||||
|
|
||||||
def test_moved_markseen(acfactory, direct_imap, log):
|
|
||||||
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
|
||||||
ac1 = acfactory.get_online_account()
|
|
||||||
|
|
||||||
addr, password = acfactory.get_credentials()
|
|
||||||
ac2 = acfactory.get_unconfigured_account()
|
|
||||||
ac2.add_or_update_transport({"addr": addr, "password": password})
|
|
||||||
ac2.bring_online()
|
|
||||||
|
|
||||||
log.section("ac2: creating DeltaChat folder")
|
|
||||||
ac2_direct_imap = direct_imap(ac2)
|
|
||||||
ac2_direct_imap.create_folder("DeltaChat")
|
|
||||||
ac2.set_config("delete_server_after", "0")
|
|
||||||
ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request.
|
|
||||||
|
|
||||||
ac2.add_or_update_transport({"addr": addr, "password": password, "imapFolder": "DeltaChat"})
|
|
||||||
ac2.bring_online()
|
|
||||||
|
|
||||||
ac2.stop_io()
|
|
||||||
ac2_direct_imap = direct_imap(ac2)
|
|
||||||
with ac2_direct_imap.idle() as idle2:
|
|
||||||
ac1.create_chat(ac2).send_text("Hello!")
|
|
||||||
idle2.wait_for_new_message()
|
|
||||||
|
|
||||||
# Emulate moving of the message to DeltaChat folder by Sieve rule.
|
|
||||||
log.section("ac2: moving message into DeltaChat folder")
|
|
||||||
ac2_direct_imap.conn.move(["*"], "DeltaChat")
|
|
||||||
ac2_direct_imap.select_folder("DeltaChat")
|
|
||||||
assert len(list(ac2_direct_imap.conn.fetch("*", mark_seen=False))) == 1
|
|
||||||
|
|
||||||
with ac2_direct_imap.idle() as idle2:
|
|
||||||
ac2.start_io()
|
|
||||||
|
|
||||||
ev = ac2.wait_for_event(EventType.MSGS_CHANGED)
|
|
||||||
msg = ac2.get_message_by_id(ev.msg_id)
|
|
||||||
assert msg.get_snapshot().text == "Messages are end-to-end encrypted."
|
|
||||||
|
|
||||||
ev = ac2.wait_for_event(EventType.INCOMING_MSG)
|
|
||||||
msg = ac2.get_message_by_id(ev.msg_id)
|
|
||||||
chat = ac2.get_chat_by_id(ev.chat_id)
|
|
||||||
|
|
||||||
# Accept the contact request.
|
|
||||||
chat.accept()
|
|
||||||
msg.mark_seen()
|
|
||||||
idle2.wait_for_seen()
|
|
||||||
|
|
||||||
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True, uid=U(1, "*")), mark_seen=False))) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_markseen_message_and_mdn(acfactory, direct_imap):
|
|
||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
for ac in ac1, ac2:
|
|
||||||
ac.set_config("delete_server_after", "0")
|
|
||||||
|
|
||||||
# Do not send BCC to self, we only want to test MDN on ac1.
|
|
||||||
ac1.set_config("bcc_self", "0")
|
|
||||||
|
|
||||||
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
|
||||||
msg = ac2.wait_for_incoming_msg()
|
|
||||||
msg.mark_seen()
|
|
||||||
|
|
||||||
rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.")
|
|
||||||
|
|
||||||
for ac in ac1, ac2:
|
|
||||||
while True:
|
|
||||||
event = ac.wait_for_event()
|
|
||||||
if event.kind == EventType.INFO and rex.search(event.msg):
|
|
||||||
break
|
|
||||||
|
|
||||||
ac1_direct_imap = direct_imap(ac1)
|
|
||||||
ac2_direct_imap = direct_imap(ac2)
|
|
||||||
|
|
||||||
ac1_direct_imap.select_folder("INBOX")
|
|
||||||
ac2_direct_imap.select_folder("INBOX")
|
|
||||||
|
|
||||||
# Check that the mdn is marked as seen
|
|
||||||
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
|
|
||||||
# Check original message is marked as seen
|
|
||||||
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_trash_multiple_messages(acfactory, direct_imap, log):
|
|
||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
|
||||||
ac2.stop_io()
|
|
||||||
|
|
||||||
ac2.set_config("delete_server_after", "0")
|
|
||||||
ac2.set_config("sync_msgs", "0")
|
|
||||||
|
|
||||||
ac2.start_io()
|
|
||||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
|
||||||
|
|
||||||
log.section("ac1: sending 3 messages")
|
|
||||||
texts = ["first", "second", "third"]
|
|
||||||
for text in texts:
|
|
||||||
chat12.send_text(text)
|
|
||||||
|
|
||||||
log.section("ac2: waiting for all messages on the other side")
|
|
||||||
to_delete = []
|
|
||||||
for text in texts:
|
|
||||||
msg = ac2.wait_for_incoming_msg().get_snapshot()
|
|
||||||
assert msg.text in texts
|
|
||||||
if text != "second":
|
|
||||||
to_delete.append(msg)
|
|
||||||
|
|
||||||
log.section("ac2: deleting all messages except second")
|
|
||||||
assert len(to_delete) == len(texts) - 1
|
|
||||||
ac2.delete_messages(to_delete)
|
|
||||||
|
|
||||||
log.section("ac2: test that only one message is left")
|
|
||||||
ac2_direct_imap = direct_imap(ac2)
|
|
||||||
while 1:
|
|
||||||
ac2.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
|
||||||
ac2_direct_imap.select_config_folder("inbox")
|
|
||||||
nr_msgs = len(ac2_direct_imap.get_all_messages())
|
|
||||||
assert nr_msgs > 0
|
|
||||||
if nr_msgs == 1:
|
|
||||||
break
|
|
||||||
@@ -24,13 +24,6 @@ def path_to_webxdc(request):
|
|||||||
return str(p)
|
return str(p)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def path_to_large_webxdc(request):
|
|
||||||
p = request.path.parent.parent.parent.joinpath("test-data/webxdc/realtime-check.xdc")
|
|
||||||
assert p.exists()
|
|
||||||
return str(p)
|
|
||||||
|
|
||||||
|
|
||||||
def log(msg):
|
def log(msg):
|
||||||
logging.info(msg)
|
logging.info(msg)
|
||||||
|
|
||||||
@@ -91,7 +84,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
|
|||||||
|
|
||||||
# share a webxdc app between ac1 and ac2
|
# share a webxdc app between ac1 and ac2
|
||||||
ac1_webxdc_msg = acfactory.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.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
|
||||||
snapshot = ac2_webxdc_msg.get_snapshot()
|
snapshot = ac2_webxdc_msg.get_snapshot()
|
||||||
assert snapshot.text == "play"
|
assert snapshot.text == "play"
|
||||||
|
|
||||||
@@ -101,7 +94,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
|
|||||||
acfactory.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.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert snapshot.text == "ping1"
|
assert snapshot.text == "ping1"
|
||||||
|
|
||||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||||
@@ -109,7 +102,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
|
|||||||
acfactory.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.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert snapshot.text == "ping2"
|
assert snapshot.text == "ping2"
|
||||||
|
|
||||||
log("sending realtime data ac1 -> ac2")
|
log("sending realtime data ac1 -> ac2")
|
||||||
@@ -221,9 +214,7 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
|||||||
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()
|
||||||
ac2_webxdc_msg_snapshot = ac2_webxdc_msg.get_snapshot()
|
assert ac2_webxdc_msg.get_snapshot().text == "WebXDC"
|
||||||
assert ac2_webxdc_msg_snapshot.text == "WebXDC"
|
|
||||||
ac2_webxdc_msg_snapshot.chat.accept()
|
|
||||||
|
|
||||||
ac1_ac2_chat.send_text("Hello!")
|
ac1_ac2_chat.send_text("Hello!")
|
||||||
ac2_hello_msg = ac2.wait_for_incoming_msg()
|
ac2_hello_msg = ac2.wait_for_incoming_msg()
|
||||||
@@ -234,29 +225,3 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
|||||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||||
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
|
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
|
||||||
assert event.msg_id == ac1_webxdc_msg.id
|
assert event.msg_id == ac1_webxdc_msg.id
|
||||||
|
|
||||||
|
|
||||||
def test_realtime_large_webxdc(acfactory, path_to_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.
|
|
||||||
Large webxdc is split into pre- and post- message,
|
|
||||||
and this previously resulted in failure to initialize realtime.
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
|
||||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="realtime check", file=path_to_large_webxdc)
|
|
||||||
|
|
||||||
# Receive pre-message.
|
|
||||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
|
||||||
|
|
||||||
# Receive post-message.
|
|
||||||
ac2_webxdc_msg = ac2.wait_for_msg(EventType.MSGS_CHANGED)
|
|
||||||
|
|
||||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
|
||||||
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
|
|
||||||
assert event.msg_id == ac1_webxdc_msg.id
|
|
||||||
|
|||||||
53
deltachat-rpc-client/tests/test_key_transfer.py
Normal file
53
deltachat-rpc-client/tests/test_key_transfer.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from deltachat_rpc_client import EventType
|
||||||
|
from deltachat_rpc_client.rpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_autocrypt_setup_message(account):
|
||||||
|
while True:
|
||||||
|
event = account.wait_for_event()
|
||||||
|
if event.kind == EventType.MSGS_CHANGED and event.msg_id != 0:
|
||||||
|
msg_id = event.msg_id
|
||||||
|
msg = account.get_message_by_id(msg_id)
|
||||||
|
if msg.get_snapshot().is_setupmessage:
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocrypt_setup_message_key_transfer(acfactory):
|
||||||
|
alice1 = acfactory.get_online_account()
|
||||||
|
|
||||||
|
alice2 = acfactory.get_unconfigured_account()
|
||||||
|
alice2.set_config("addr", alice1.get_config("addr"))
|
||||||
|
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
||||||
|
alice2.configure()
|
||||||
|
alice2.bring_online()
|
||||||
|
|
||||||
|
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||||
|
msg = wait_for_autocrypt_setup_message(alice2)
|
||||||
|
|
||||||
|
# Test that entering wrong code returns an error.
|
||||||
|
with pytest.raises(JsonRpcError):
|
||||||
|
msg.continue_autocrypt_key_transfer("7037-0673-6287-3013-4095-7956-5617-6806-6756")
|
||||||
|
|
||||||
|
msg.continue_autocrypt_key_transfer(setup_code)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ac_setup_message_twice(acfactory):
|
||||||
|
alice1 = acfactory.get_online_account()
|
||||||
|
|
||||||
|
alice2 = acfactory.get_unconfigured_account()
|
||||||
|
alice2.set_config("addr", alice1.get_config("addr"))
|
||||||
|
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
||||||
|
alice2.configure()
|
||||||
|
alice2.bring_online()
|
||||||
|
|
||||||
|
# Send the first Autocrypt Setup Message and ignore it.
|
||||||
|
_setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||||
|
wait_for_autocrypt_setup_message(alice2)
|
||||||
|
|
||||||
|
# Send the second Autocrypt Setup Message and import it.
|
||||||
|
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||||
|
msg = wait_for_autocrypt_setup_message(alice2)
|
||||||
|
|
||||||
|
msg.continue_autocrypt_key_transfer(setup_code)
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
def test_set_location(dc, acfactory) -> None:
|
|
||||||
# Try setting location without any accounts.
|
|
||||||
assert not dc.set_location(1.0, 2.0, 0.1)
|
|
||||||
|
|
||||||
# Create one account that does not stream,
|
|
||||||
# set location.
|
|
||||||
acfactory.new_configured_account()
|
|
||||||
assert not dc.set_location(3.0, 4.0, 0.1)
|
|
||||||
|
|
||||||
|
|
||||||
def test_send_locations_to_chat(dc, acfactory):
|
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
assert not alice.is_sending_locations()
|
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
|
||||||
assert not alice_chat_bob.is_sending_locations()
|
|
||||||
|
|
||||||
# Test starting and stopping location streaming in a chat.
|
|
||||||
alice_chat_bob.send_locations(3600)
|
|
||||||
assert alice.is_sending_locations()
|
|
||||||
assert alice_chat_bob.is_sending_locations()
|
|
||||||
alice_chat_bob.send_locations(0)
|
|
||||||
assert not alice.is_sending_locations()
|
|
||||||
assert not alice_chat_bob.is_sending_locations()
|
|
||||||
|
|
||||||
# Test stop_sending_locations() for all accounts and chats.
|
|
||||||
alice_chat_bob.send_locations(3600)
|
|
||||||
assert alice.is_sending_locations()
|
|
||||||
assert alice_chat_bob.is_sending_locations()
|
|
||||||
dc.stop_sending_locations()
|
|
||||||
assert not alice.is_sending_locations()
|
|
||||||
assert not alice_chat_bob.is_sending_locations()
|
|
||||||
@@ -4,41 +4,6 @@ from deltachat_rpc_client import EventType
|
|||||||
from deltachat_rpc_client.const import MessageState
|
from deltachat_rpc_client.const import MessageState
|
||||||
|
|
||||||
|
|
||||||
def test_bcc_self_delete_server_after_defaults(acfactory):
|
|
||||||
"""Test default values for bcc_self and delete_server_after."""
|
|
||||||
ac = acfactory.get_online_account()
|
|
||||||
|
|
||||||
# Initially after getting online
|
|
||||||
# the setting bcc_self is set to 0 because there is only one device
|
|
||||||
# and delete_server_after is "1", meaning immediate deletion.
|
|
||||||
assert ac.get_config("bcc_self") == "0"
|
|
||||||
assert ac.get_config("delete_server_after") == "1"
|
|
||||||
|
|
||||||
# Setup a second device.
|
|
||||||
ac_clone = ac.clone()
|
|
||||||
ac_clone.bring_online()
|
|
||||||
|
|
||||||
# Second device setup
|
|
||||||
# enables bcc_self and changes default delete_server_after.
|
|
||||||
assert ac.get_config("bcc_self") == "1"
|
|
||||||
assert ac.get_config("delete_server_after") == "0"
|
|
||||||
|
|
||||||
assert ac_clone.get_config("bcc_self") == "1"
|
|
||||||
assert ac_clone.get_config("delete_server_after") == "0"
|
|
||||||
|
|
||||||
# Manually disabling bcc_self
|
|
||||||
# also restores the default for delete_server_after.
|
|
||||||
ac.set_config("bcc_self", "0")
|
|
||||||
assert ac.get_config("bcc_self") == "0"
|
|
||||||
assert ac.get_config("delete_server_after") == "1"
|
|
||||||
|
|
||||||
# Cloning the account again enables bcc_self
|
|
||||||
# even though it was manually disabled.
|
|
||||||
ac_clone = ac.clone()
|
|
||||||
assert ac.get_config("bcc_self") == "1"
|
|
||||||
assert ac.get_config("delete_server_after") == "0"
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
ac1_clone = ac1.clone()
|
ac1_clone = ac1.clone()
|
||||||
@@ -71,9 +36,6 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
|||||||
assert ac1.get_config("bcc_self") == "1"
|
assert ac1.get_config("bcc_self") == "1"
|
||||||
|
|
||||||
# Second client receives only 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)
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -1,313 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from deltachat_rpc_client import EventType
|
|
||||||
from deltachat_rpc_client.const import ChatType, DownloadState
|
|
||||||
from deltachat_rpc_client.rpc import JsonRpcError
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_second_address(acfactory) -> None:
|
|
||||||
account = acfactory.new_configured_account()
|
|
||||||
assert len(account.list_transports()) == 1
|
|
||||||
|
|
||||||
qr = acfactory.get_account_qr()
|
|
||||||
account.add_transport_from_qr(qr)
|
|
||||||
assert len(account.list_transports()) == 2
|
|
||||||
|
|
||||||
account.add_transport_from_qr(qr)
|
|
||||||
assert len(account.list_transports()) == 3
|
|
||||||
|
|
||||||
first_addr = account.list_transports()[0]["addr"]
|
|
||||||
second_addr = account.list_transports()[1]["addr"]
|
|
||||||
|
|
||||||
# Cannot delete the first address.
|
|
||||||
with pytest.raises(JsonRpcError):
|
|
||||||
account.delete_transport(first_addr)
|
|
||||||
|
|
||||||
account.delete_transport(second_addr)
|
|
||||||
assert len(account.list_transports()) == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_change_address(acfactory) -> None:
|
|
||||||
"""Test Alice configuring a second transport and setting it as a primary one."""
|
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob_addr = bob.get_config("configured_addr")
|
|
||||||
bob.create_chat(alice)
|
|
||||||
|
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
|
||||||
alice_chat_bob.send_text("Hello!")
|
|
||||||
|
|
||||||
msg1 = bob.wait_for_incoming_msg().get_snapshot()
|
|
||||||
sender_addr1 = msg1.sender.get_snapshot().address
|
|
||||||
|
|
||||||
alice.stop_io()
|
|
||||||
old_alice_addr = alice.get_config("configured_addr")
|
|
||||||
alice_vcard = alice.self_contact.make_vcard()
|
|
||||||
assert old_alice_addr in alice_vcard
|
|
||||||
qr = acfactory.get_account_qr()
|
|
||||||
alice.add_transport_from_qr(qr)
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
assert old_alice_addr not in alice_vcard
|
|
||||||
assert new_alice_addr in alice_vcard
|
|
||||||
with pytest.raises(JsonRpcError):
|
|
||||||
alice.delete_transport(new_alice_addr)
|
|
||||||
alice.start_io()
|
|
||||||
|
|
||||||
alice_chat_bob.send_text("Hello again!")
|
|
||||||
|
|
||||||
msg2 = bob.wait_for_incoming_msg().get_snapshot()
|
|
||||||
sender_addr2 = msg2.sender.get_snapshot().address
|
|
||||||
|
|
||||||
assert msg1.sender == msg2.sender
|
|
||||||
assert sender_addr1 != sender_addr2
|
|
||||||
assert sender_addr1 == old_alice_addr
|
|
||||||
assert sender_addr2 == new_alice_addr
|
|
||||||
|
|
||||||
|
|
||||||
def test_download_on_demand(acfactory) -> None:
|
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
|
||||||
alice.set_config("download_limit", "1")
|
|
||||||
|
|
||||||
alice.stop_io()
|
|
||||||
qr = acfactory.get_account_qr()
|
|
||||||
alice.add_transport_from_qr(qr)
|
|
||||||
alice.start_io()
|
|
||||||
|
|
||||||
alice.create_chat(bob)
|
|
||||||
chat_bob_alice = bob.create_chat(alice)
|
|
||||||
chat_bob_alice.send_message(file="../test-data/image/screenshot.jpg")
|
|
||||||
msg = alice.wait_for_incoming_msg()
|
|
||||||
snapshot = msg.get_snapshot()
|
|
||||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
|
||||||
chat_id = snapshot.chat_id
|
|
||||||
# Actually the message isn't available yet. Wait somehow for the post-message to arrive.
|
|
||||||
chat_bob_alice.send_message("Now you can download my previous message")
|
|
||||||
alice.wait_for_incoming_msg()
|
|
||||||
alice._rpc.download_full_message(alice.id, msg.id)
|
|
||||||
for dstate in [DownloadState.IN_PROGRESS, DownloadState.DONE]:
|
|
||||||
event = alice.wait_for_event(EventType.MSGS_CHANGED)
|
|
||||||
assert event.chat_id == chat_id
|
|
||||||
assert event.msg_id == msg.id
|
|
||||||
assert msg.get_snapshot().download_state == dstate
|
|
||||||
|
|
||||||
|
|
||||||
def test_reconfigure_transport(acfactory) -> None:
|
|
||||||
"""Test that reconfiguring the transport works."""
|
|
||||||
account = acfactory.get_online_account()
|
|
||||||
|
|
||||||
[transport] = account.list_transports()
|
|
||||||
account.add_or_update_transport(transport)
|
|
||||||
|
|
||||||
|
|
||||||
def test_transport_synchronization(acfactory, log) -> None:
|
|
||||||
"""Test synchronization of transports between devices."""
|
|
||||||
|
|
||||||
def wait_for_io_started(ac):
|
|
||||||
while True:
|
|
||||||
ev = ac.wait_for_event(EventType.INFO)
|
|
||||||
if "scheduler is running" in ev.msg:
|
|
||||||
return
|
|
||||||
|
|
||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
|
||||||
ac1_clone = ac1.clone()
|
|
||||||
ac1_clone.bring_online()
|
|
||||||
|
|
||||||
qr = acfactory.get_account_qr()
|
|
||||||
|
|
||||||
ac1.add_transport_from_qr(qr)
|
|
||||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
|
||||||
wait_for_io_started(ac1_clone)
|
|
||||||
assert len(ac1.list_transports()) == 2
|
|
||||||
assert len(ac1_clone.list_transports()) == 2
|
|
||||||
|
|
||||||
ac1_clone.add_transport_from_qr(qr)
|
|
||||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
|
||||||
wait_for_io_started(ac1)
|
|
||||||
assert len(ac1.list_transports()) == 3
|
|
||||||
assert len(ac1_clone.list_transports()) == 3
|
|
||||||
|
|
||||||
log.section("ac1 clone removes second transport")
|
|
||||||
[transport1, transport2, transport3] = ac1_clone.list_transports()
|
|
||||||
addr3 = transport3["addr"]
|
|
||||||
ac1_clone.delete_transport(transport2["addr"])
|
|
||||||
|
|
||||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
|
||||||
wait_for_io_started(ac1)
|
|
||||||
[transport1, transport3] = ac1.list_transports()
|
|
||||||
|
|
||||||
log.section("ac1 changes the primary transport")
|
|
||||||
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")
|
|
||||||
ac1.delete_transport(transport1["addr"])
|
|
||||||
|
|
||||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
|
||||||
wait_for_io_started(ac1_clone)
|
|
||||||
[transport3] = ac1_clone.list_transports()
|
|
||||||
assert transport3["addr"] == addr3
|
|
||||||
assert ac1_clone.get_config("configured_addr") == addr3
|
|
||||||
|
|
||||||
ac2_chat = ac2.create_chat(ac1)
|
|
||||||
ac2_chat.send_text("Hello!")
|
|
||||||
|
|
||||||
assert ac1.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(acfactory, log) -> None:
|
|
||||||
"""Test synchronization of new transport as primary between devices."""
|
|
||||||
ac1, bob = acfactory.get_online_accounts(2)
|
|
||||||
ac1_clone = ac1.clone()
|
|
||||||
ac1_clone.bring_online()
|
|
||||||
|
|
||||||
qr = acfactory.get_account_qr()
|
|
||||||
|
|
||||||
ac1.add_transport_from_qr(qr)
|
|
||||||
ac1_transports = ac1.list_transports()
|
|
||||||
assert len(ac1_transports) == 2
|
|
||||||
[transport1, transport2] = ac1_transports
|
|
||||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
|
||||||
assert len(ac1_clone.list_transports()) == 2
|
|
||||||
assert ac1_clone.get_config("configured_addr") == transport1["addr"]
|
|
||||||
|
|
||||||
log.section("ac1 changes the primary transport")
|
|
||||||
ac1.set_config("configured_addr", transport2["addr"])
|
|
||||||
|
|
||||||
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.send_text("Hello!")
|
|
||||||
bob_chat_id = bob.wait_for_incoming_msg_event().chat_id
|
|
||||||
bob_chat = bob.get_chat_by_id(bob_chat_id)
|
|
||||||
bob_chat.accept()
|
|
||||||
bob_chat.send_text("hello back")
|
|
||||||
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "hello back"
|
|
||||||
|
|
||||||
|
|
||||||
def test_recognize_self_address(acfactory) -> None:
|
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob_chat = bob.create_chat(alice)
|
|
||||||
|
|
||||||
qr = acfactory.get_account_qr()
|
|
||||||
alice.add_transport_from_qr(qr)
|
|
||||||
|
|
||||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
|
||||||
alice.set_config("configured_addr", new_alice_addr)
|
|
||||||
|
|
||||||
bob_chat.send_text("Hello!")
|
|
||||||
msg = alice.wait_for_incoming_msg().get_snapshot()
|
|
||||||
assert msg.chat == alice.create_chat(bob)
|
|
||||||
|
|
||||||
|
|
||||||
def test_transport_limit(acfactory) -> None:
|
|
||||||
"""Test transports limit."""
|
|
||||||
account = acfactory.get_online_account()
|
|
||||||
qr = acfactory.get_account_qr()
|
|
||||||
|
|
||||||
limit = 5
|
|
||||||
|
|
||||||
for _ in range(1, limit):
|
|
||||||
account.add_transport_from_qr(qr)
|
|
||||||
|
|
||||||
assert len(account.list_transports()) == limit
|
|
||||||
|
|
||||||
with pytest.raises(JsonRpcError):
|
|
||||||
account.add_transport_from_qr(qr)
|
|
||||||
|
|
||||||
second_addr = account.list_transports()[1]["addr"]
|
|
||||||
account.delete_transport(second_addr)
|
|
||||||
|
|
||||||
# test that adding a transport after deleting one works again
|
|
||||||
account.add_transport_from_qr(qr)
|
|
||||||
|
|
||||||
|
|
||||||
def test_message_info_imap_urls(acfactory) -> None:
|
|
||||||
"""Test that message info contains IMAP URLs of where the message was received."""
|
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
qr = acfactory.get_account_qr()
|
|
||||||
for i in range(3):
|
|
||||||
alice.add_transport_from_qr(qr)
|
|
||||||
# Wait for all transports to go IDLE after adding each one.
|
|
||||||
for _ in range(i + 1):
|
|
||||||
alice.bring_online()
|
|
||||||
|
|
||||||
# Enable multi-device mode so messages are not deleted immediately.
|
|
||||||
alice.set_config("bcc_self", "1")
|
|
||||||
|
|
||||||
# Bob creates chat, learning about Alice's currently selected transport.
|
|
||||||
# This is where he will send the message.
|
|
||||||
bob_chat = bob.create_chat(alice)
|
|
||||||
|
|
||||||
# Alice switches to another transport and removes the rest of the transports.
|
|
||||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
|
||||||
alice.set_config("configured_addr", new_alice_addr)
|
|
||||||
removed_addrs = []
|
|
||||||
for transport in alice.list_transports():
|
|
||||||
if transport["addr"] != new_alice_addr:
|
|
||||||
alice.delete_transport(transport["addr"])
|
|
||||||
removed_addrs.append(transport["addr"])
|
|
||||||
alice.stop_io()
|
|
||||||
alice.start_io()
|
|
||||||
|
|
||||||
bob_chat.send_text("Hello!")
|
|
||||||
|
|
||||||
msg = alice.wait_for_incoming_msg()
|
|
||||||
msg_info = msg.get_info()
|
|
||||||
assert new_alice_addr in msg_info
|
|
||||||
for removed_addr in removed_addrs:
|
|
||||||
assert removed_addr not in msg_info
|
|
||||||
assert f"{new_alice_addr}/INBOX" in msg_info
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_primary_transport(acfactory, log) -> None:
|
|
||||||
"""Test that after removing the primary relay, Alice can still receive messages."""
|
|
||||||
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")
|
|
||||||
[transport1, transport2] = alice.list_transports()
|
|
||||||
alice.set_config("configured_addr", transport2["addr"])
|
|
||||||
|
|
||||||
bob_chat.send_text("Hello!")
|
|
||||||
msg1 = alice.wait_for_incoming_msg().get_snapshot()
|
|
||||||
assert msg1.text == "Hello!"
|
|
||||||
|
|
||||||
log.section("Alice removes the primary relay")
|
|
||||||
alice.delete_transport(transport1["addr"])
|
|
||||||
alice.stop_io()
|
|
||||||
alice.start_io()
|
|
||||||
|
|
||||||
bob_chat.send_text("Hello again!")
|
|
||||||
msg2 = alice.wait_for_incoming_msg().get_snapshot()
|
|
||||||
assert msg2.text == "Hello again!"
|
|
||||||
assert msg2.chat.get_basic_snapshot().chat_type == ChatType.SINGLE
|
|
||||||
assert msg2.chat == alice.create_chat(bob)
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
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.rpc import JsonRpcError
|
from deltachat_rpc_client.rpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
@@ -16,14 +16,14 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
|||||||
alice.wait_for_securejoin_inviter_success()
|
alice.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
# Test that Alice verified Bob's profile.
|
# Test that Alice verified Bob's profile.
|
||||||
alice_contact_bob = alice.create_contact(bob)
|
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||||
assert alice_contact_bob_snapshot.is_verified
|
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.
|
# Test that Bob verified Alice's profile.
|
||||||
bob_contact_alice = bob.create_contact(alice)
|
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
||||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||||
assert bob_contact_alice_snapshot.is_verified
|
assert bob_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
@@ -59,7 +59,8 @@ def test_qr_setup_contact_svg(acfactory) -> None:
|
|||||||
assert "Alice" in svg
|
assert "Alice" in svg
|
||||||
|
|
||||||
|
|
||||||
def test_qr_securejoin(acfactory):
|
@pytest.mark.parametrize("protect", [True, False])
|
||||||
|
def test_qr_securejoin(acfactory, protect):
|
||||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
# Setup second device for Alice
|
# Setup second device for Alice
|
||||||
@@ -67,7 +68,8 @@ def test_qr_securejoin(acfactory):
|
|||||||
alice2 = alice.clone()
|
alice2 = alice.clone()
|
||||||
|
|
||||||
logging.info("Alice creates a group")
|
logging.info("Alice creates a group")
|
||||||
alice_chat = alice.create_group("Group")
|
alice_chat = alice.create_group("Group", protect=protect)
|
||||||
|
assert alice_chat.get_basic_snapshot().is_protected == protect
|
||||||
|
|
||||||
logging.info("Bob joins the group")
|
logging.info("Bob joins the group")
|
||||||
qr_code = alice_chat.get_qr_code()
|
qr_code = alice_chat.get_qr_code()
|
||||||
@@ -82,15 +84,16 @@ def test_qr_securejoin(acfactory):
|
|||||||
bob.wait_for_securejoin_joiner_success()
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
# Test that Alice verified Bob's profile.
|
# Test that Alice verified Bob's profile.
|
||||||
alice_contact_bob = alice.create_contact(bob)
|
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||||
assert alice_contact_bob_snapshot.is_verified
|
assert alice_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||||
|
assert snapshot.chat.get_basic_snapshot().is_protected == protect
|
||||||
|
|
||||||
# Test that Bob verified Alice's profile.
|
# Test that Bob verified Alice's profile.
|
||||||
bob_contact_alice = bob.create_contact(alice)
|
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
||||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||||
assert bob_contact_alice_snapshot.is_verified
|
assert bob_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
@@ -98,7 +101,7 @@ def test_qr_securejoin(acfactory):
|
|||||||
# Alice observes securejoin protocol and verifies Bob on 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.get_contact_by_addr(bob.get_config("addr"))
|
||||||
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
|
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
|
||||||
assert alice2_contact_bob_snapshot.is_verified
|
assert alice2_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
@@ -110,148 +113,6 @@ def test_qr_securejoin(acfactory):
|
|||||||
fiona.wait_for_securejoin_joiner_success()
|
fiona.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("all_devices_online", [True, False])
|
|
||||||
def test_qr_securejoin_broadcast(acfactory, all_devices_online):
|
|
||||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
|
||||||
|
|
||||||
alice2 = alice.clone()
|
|
||||||
bob2 = bob.clone()
|
|
||||||
|
|
||||||
if all_devices_online:
|
|
||||||
alice2.start_io()
|
|
||||||
bob2.start_io()
|
|
||||||
|
|
||||||
logging.info("===================== Alice creates a broadcast =====================")
|
|
||||||
alice_chat = alice.create_broadcast("Broadcast channel!")
|
|
||||||
snapshot = alice_chat.get_basic_snapshot()
|
|
||||||
assert not snapshot.is_unpromoted # Broadcast channels are never unpromoted
|
|
||||||
|
|
||||||
logging.info("===================== Bob joins the broadcast =====================")
|
|
||||||
|
|
||||||
qr_code = alice_chat.get_qr_code()
|
|
||||||
bob.secure_join(qr_code)
|
|
||||||
alice.wait_for_securejoin_inviter_success()
|
|
||||||
bob.wait_for_securejoin_joiner_success()
|
|
||||||
alice_chat.send_text("Hello everyone!")
|
|
||||||
|
|
||||||
def get_broadcast(ac):
|
|
||||||
chat = ac.get_chatlist(query="Broadcast channel!")[0]
|
|
||||||
assert chat.get_basic_snapshot().name == "Broadcast channel!"
|
|
||||||
return chat
|
|
||||||
|
|
||||||
def wait_for_broadcast_messages(ac):
|
|
||||||
snapshot1 = ac.wait_for_incoming_msg().get_snapshot()
|
|
||||||
assert snapshot1.text == "You joined the channel."
|
|
||||||
|
|
||||||
snapshot2 = ac.wait_for_incoming_msg().get_snapshot()
|
|
||||||
assert snapshot2.text == "Hello everyone!"
|
|
||||||
|
|
||||||
chat = get_broadcast(ac)
|
|
||||||
assert snapshot1.chat_id == chat.id
|
|
||||||
assert snapshot2.chat_id == chat.id
|
|
||||||
|
|
||||||
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
|
|
||||||
# Check that the chat partner is verified.
|
|
||||||
contact_snapshot = contact.get_snapshot()
|
|
||||||
assert contact_snapshot.is_verified
|
|
||||||
|
|
||||||
chat = get_broadcast(ac)
|
|
||||||
chat_msgs = chat.get_messages()
|
|
||||||
|
|
||||||
encrypted_msg = chat_msgs.pop(0).get_snapshot()
|
|
||||||
assert encrypted_msg.text == "Messages are end-to-end encrypted."
|
|
||||||
assert encrypted_msg.is_info
|
|
||||||
|
|
||||||
if please_wait_info_msg:
|
|
||||||
first_msg = chat_msgs.pop(0).get_snapshot()
|
|
||||||
assert "invited you to join this channel" in first_msg.text
|
|
||||||
assert first_msg.is_info
|
|
||||||
|
|
||||||
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.":
|
|
||||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
|
||||||
else:
|
|
||||||
member_added_msg = chat_msgs.pop(1).get_snapshot()
|
|
||||||
assert member_added_msg.text == "You joined the channel."
|
|
||||||
assert member_added_msg.is_info
|
|
||||||
|
|
||||||
hello_msg = chat_msgs.pop(0).get_snapshot()
|
|
||||||
assert hello_msg.text == "Hello everyone!"
|
|
||||||
assert not hello_msg.is_info
|
|
||||||
assert hello_msg.show_padlock
|
|
||||||
assert hello_msg.error is None
|
|
||||||
|
|
||||||
assert len(chat_msgs) == 0
|
|
||||||
|
|
||||||
chat_snapshot = chat.get_full_snapshot()
|
|
||||||
assert chat_snapshot.is_encrypted
|
|
||||||
assert chat_snapshot.name == "Broadcast channel!"
|
|
||||||
if inviter_side:
|
|
||||||
assert chat_snapshot.chat_type == ChatType.OUT_BROADCAST
|
|
||||||
else:
|
|
||||||
assert chat_snapshot.chat_type == ChatType.IN_BROADCAST
|
|
||||||
assert chat_snapshot.can_send == inviter_side
|
|
||||||
|
|
||||||
chat_contacts = chat_snapshot.contact_ids
|
|
||||||
assert contact.id in chat_contacts
|
|
||||||
if inviter_side:
|
|
||||||
assert len(chat_contacts) == 1
|
|
||||||
else:
|
|
||||||
assert len(chat_contacts) == 2
|
|
||||||
assert SpecialContactId.SELF in chat_contacts
|
|
||||||
assert chat_snapshot.self_in_group
|
|
||||||
|
|
||||||
wait_for_broadcast_messages(bob)
|
|
||||||
|
|
||||||
check_account(alice, alice.create_contact(bob), inviter_side=True)
|
|
||||||
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
|
|
||||||
|
|
||||||
logging.info("===================== Test Alice's second device =====================")
|
|
||||||
|
|
||||||
# Start second Alice device, if it wasn't started already.
|
|
||||||
alice2.start_io()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
msg_id = alice2.wait_for_msgs_changed_event().msg_id
|
|
||||||
if msg_id:
|
|
||||||
snapshot = alice2.get_message_by_id(msg_id).get_snapshot()
|
|
||||||
if snapshot.text == "Hello everyone!":
|
|
||||||
break
|
|
||||||
|
|
||||||
check_account(alice2, alice2.create_contact(bob), inviter_side=True)
|
|
||||||
|
|
||||||
logging.info("===================== Test Bob's second device =====================")
|
|
||||||
|
|
||||||
# Start second Bob device, if it wasn't started already.
|
|
||||||
bob2.start_io()
|
|
||||||
bob2.wait_for_securejoin_joiner_success()
|
|
||||||
wait_for_broadcast_messages(bob2)
|
|
||||||
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
|
|
||||||
|
|
||||||
# The QR code token is synced, so alice2 must be able to handle join requests.
|
|
||||||
logging.info("===================== Fiona joins the group via alice2 =====================")
|
|
||||||
alice.stop_io()
|
|
||||||
fiona.secure_join(qr_code)
|
|
||||||
alice2.wait_for_securejoin_inviter_success()
|
|
||||||
fiona.wait_for_securejoin_joiner_success()
|
|
||||||
|
|
||||||
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
|
|
||||||
assert snapshot.text == "You joined the channel."
|
|
||||||
|
|
||||||
get_broadcast(alice2).get_messages()[2].resend()
|
|
||||||
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
|
|
||||||
assert snapshot.text == "Hello everyone!"
|
|
||||||
|
|
||||||
check_account(fiona, fiona.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
|
|
||||||
|
|
||||||
# For Bob, the channel must not have changed:
|
|
||||||
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
|
|
||||||
|
|
||||||
|
|
||||||
def test_qr_securejoin_contact_request(acfactory) -> 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 = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
@@ -260,13 +121,13 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
|
|||||||
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!")
|
||||||
|
|
||||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert snapshot.text == "Hello!"
|
assert snapshot.text == "Hello!"
|
||||||
bob_chat_alice = snapshot.chat
|
bob_chat_alice = snapshot.chat
|
||||||
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
||||||
|
|
||||||
alice_chat = alice.create_group("Group")
|
alice_chat = alice.create_group("Verified group", protect=True)
|
||||||
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.secure_join(qr_code)
|
bob.secure_join(qr_code)
|
||||||
while True:
|
while True:
|
||||||
@@ -290,8 +151,8 @@ def test_qr_readreceipt(acfactory) -> None:
|
|||||||
for joiner in [bob, charlie]:
|
for joiner in [bob, charlie]:
|
||||||
joiner.wait_for_securejoin_joiner_success()
|
joiner.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
logging.info("Alice creates a group")
|
logging.info("Alice creates a verified group")
|
||||||
group = alice.create_group("Group")
|
group = alice.create_group("Group", protect=True)
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
|
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
|
||||||
@@ -304,7 +165,8 @@ def test_qr_readreceipt(acfactory) -> None:
|
|||||||
|
|
||||||
logging.info("Bob and Charlie receive a group")
|
logging.info("Bob and Charlie receive a group")
|
||||||
|
|
||||||
bob_message = bob.wait_for_incoming_msg()
|
bob_msg_id = bob.wait_for_incoming_msg_event().msg_id
|
||||||
|
bob_message = bob.get_message_by_id(bob_msg_id)
|
||||||
bob_snapshot = bob_message.get_snapshot()
|
bob_snapshot = bob_message.get_snapshot()
|
||||||
assert bob_snapshot.text == "Hello"
|
assert bob_snapshot.text == "Hello"
|
||||||
|
|
||||||
@@ -315,7 +177,8 @@ def test_qr_readreceipt(acfactory) -> None:
|
|||||||
|
|
||||||
bob_out_message = bob_snapshot.chat.send_message(text="Hi from Bob!")
|
bob_out_message = bob_snapshot.chat.send_message(text="Hi from Bob!")
|
||||||
|
|
||||||
charlie_message = charlie.wait_for_incoming_msg()
|
charlie_msg_id = charlie.wait_for_incoming_msg_event().msg_id
|
||||||
|
charlie_message = charlie.get_message_by_id(charlie_msg_id)
|
||||||
charlie_snapshot = charlie_message.get_snapshot()
|
charlie_snapshot = charlie_message.get_snapshot()
|
||||||
assert charlie_snapshot.text == "Hi from Bob!"
|
assert charlie_snapshot.text == "Hi from Bob!"
|
||||||
|
|
||||||
@@ -350,20 +213,21 @@ def test_setup_contact_resetup(acfactory) -> None:
|
|||||||
bob.wait_for_securejoin_joiner_success()
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
|
||||||
def test_verified_group_member_added_recovery(acfactory) -> None:
|
def test_verified_group_recovery(acfactory) -> None:
|
||||||
"""Tests verified group recovery by reverifying then removing and adding a member back."""
|
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
|
||||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
logging.info("ac1 creates a group")
|
logging.info("ac1 creates verified group")
|
||||||
chat = ac1.create_group("Group")
|
chat = ac1.create_group("Verified group", protect=True)
|
||||||
|
assert chat.get_basic_snapshot().is_protected
|
||||||
|
|
||||||
logging.info("ac2 joins the group")
|
logging.info("ac2 joins verified group")
|
||||||
qr_code = chat.get_qr_code()
|
qr_code = chat.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 has ac2 directly verified.
|
# ac1 has ac2 directly verified.
|
||||||
ac1_contact_ac2 = ac1.create_contact(ac2)
|
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||||
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||||
|
|
||||||
logging.info("ac3 joins verified group")
|
logging.info("ac3 joins verified group")
|
||||||
@@ -371,8 +235,6 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
|||||||
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
|
||||||
|
|
||||||
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 = acfactory.resetup_account(ac2)
|
ac2 = acfactory.resetup_account(ac2)
|
||||||
|
|
||||||
@@ -385,12 +247,87 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
|||||||
assert len(ac3_chat.get_contacts()) == 3
|
assert len(ac3_chat.get_contacts()) == 3
|
||||||
ac3_chat.send_text("Hi!")
|
ac3_chat.send_text("Hi!")
|
||||||
|
|
||||||
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hi!"
|
||||||
|
|
||||||
|
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||||
|
message = ac2.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert snapshot.text == "Hi!"
|
||||||
|
|
||||||
|
# ac1 contact is verified for ac2 because ac3 gossiped ac1 key in the "Hi!" message.
|
||||||
|
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
|
||||||
|
assert ac1_contact.get_snapshot().is_verified
|
||||||
|
|
||||||
|
# ac2 can write messages to the group.
|
||||||
|
snapshot.chat.send_text("Works again!")
|
||||||
|
|
||||||
|
snapshot = ac3.get_message_by_id(ac3.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
|
ac1_chat_messages = snapshot.chat.get_messages()
|
||||||
|
ac2_addr = ac2.get_config("addr")
|
||||||
|
assert ac1_chat_messages[-2].get_snapshot().text == f"Changed setup for {ac2_addr}"
|
||||||
|
|
||||||
|
# ac2 is now verified by ac3 for ac1
|
||||||
|
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
|
||||||
|
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||||
|
"""Tests verified group recovery by reverifiying than removing and adding a member back."""
|
||||||
|
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
|
logging.info("ac1 creates verified group")
|
||||||
|
chat = ac1.create_group("Verified group", protect=True)
|
||||||
|
assert chat.get_basic_snapshot().is_protected
|
||||||
|
|
||||||
|
logging.info("ac2 joins verified group")
|
||||||
|
qr_code = chat.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# ac1 has ac2 directly verified.
|
||||||
|
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||||
|
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||||
|
|
||||||
|
logging.info("ac3 joins verified group")
|
||||||
|
ac3_chat = ac3.secure_join(qr_code)
|
||||||
|
ac3.wait_for_securejoin_joiner_success()
|
||||||
|
ac3.wait_for_incoming_msg_event() # Member added
|
||||||
|
|
||||||
|
logging.info("ac2 logs in on a new device")
|
||||||
|
ac2 = acfactory.resetup_account(ac2)
|
||||||
|
|
||||||
|
logging.info("ac2 reverifies with ac3")
|
||||||
|
qr_code = ac3.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
logging.info("ac3 sends a message to the group")
|
||||||
|
assert len(ac3_chat.get_contacts()) == 3
|
||||||
|
ac3_chat.send_text("Hi!")
|
||||||
|
|
||||||
|
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||||
|
message = ac2.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
logging.info("Received message %s", snapshot.text)
|
||||||
|
assert snapshot.text == "Hi!"
|
||||||
|
|
||||||
ac1.wait_for_incoming_msg_event() # Hi!
|
ac1.wait_for_incoming_msg_event() # Hi!
|
||||||
|
|
||||||
ac3_contact_ac2 = ac3.create_contact(ac2)
|
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
|
||||||
ac3_chat.remove_contact(ac3_contact_ac2_old)
|
ac3_chat.remove_contact(ac3_contact_ac2)
|
||||||
|
|
||||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||||
|
message = ac2.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert "removed" in snapshot.text
|
||||||
|
|
||||||
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert "removed" in snapshot.text
|
assert "removed" in snapshot.text
|
||||||
|
|
||||||
ac3_chat.add_contact(ac3_contact_ac2)
|
ac3_chat.add_contact(ac3_contact_ac2)
|
||||||
@@ -403,31 +340,33 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
|||||||
logging.info("ac2 got event message: %s", snapshot.text)
|
logging.info("ac2 got event message: %s", snapshot.text)
|
||||||
assert "added" in snapshot.text
|
assert "added" in snapshot.text
|
||||||
|
|
||||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert "added" in snapshot.text
|
assert "added" in snapshot.text
|
||||||
|
|
||||||
chat = Chat(ac2, chat_id)
|
chat = Chat(ac2, chat_id)
|
||||||
chat.send_text("Works again!")
|
chat.send_text("Works again!")
|
||||||
|
|
||||||
message = ac3.wait_for_incoming_msg()
|
msg_id = ac3.wait_for_incoming_msg_event().msg_id
|
||||||
|
message = ac3.get_message_by_id(msg_id)
|
||||||
snapshot = message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
assert snapshot.text == "Works again!"
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert snapshot.text == "Works again!"
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
ac1_contact_ac2 = ac1.create_contact(ac2)
|
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||||
ac1_contact_ac3 = ac1.create_contact(ac3)
|
|
||||||
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
||||||
# Until we reset verifications and then send the _verified header,
|
assert ac1_contact_ac2_snapshot.is_verified
|
||||||
# verification is not gossiped here:
|
assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id
|
||||||
assert not ac1_contact_ac2_snapshot.is_verified
|
|
||||||
assert ac1_contact_ac2_snapshot.verifier_id != ac1_contact_ac3.id
|
# ac2 is now verified by ac3 for ac1
|
||||||
|
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
|
||||||
|
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
|
||||||
|
|
||||||
|
|
||||||
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
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/deltachat/deltachat-core-rust/issues/4894>.
|
||||||
"""
|
"""
|
||||||
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
||||||
|
|
||||||
@@ -440,8 +379,8 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
|||||||
# we first create a fully joined verified 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 verified group that ac2 fully joins")
|
||||||
ch1 = ac1.create_group("Group")
|
ch1 = ac1.create_group("Group", protect=True)
|
||||||
qr_code = ch1.get_qr_code()
|
qr_code = ch1.get_qr_code()
|
||||||
ac2.secure_join(qr_code)
|
ac2.secure_join(qr_code)
|
||||||
ac1.wait_for_securejoin_inviter_success()
|
ac1.wait_for_securejoin_inviter_success()
|
||||||
@@ -449,8 +388,9 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
|||||||
# ensure ac1 can write and ac2 receives messages in verified 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.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
if snapshot.text == "ac1 says hello":
|
if snapshot.text == "ac1 says hello":
|
||||||
|
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||||
break
|
break
|
||||||
|
|
||||||
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
|
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
|
||||||
@@ -460,18 +400,19 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
|||||||
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 ac3/ac2 verification 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().is_verified
|
assert ac3.get_contact_by_addr(ac2.get_config("addr")).get_snapshot().is_verified
|
||||||
assert ac2.create_contact(ac3).get_snapshot().is_verified
|
assert ac2.get_contact_by_addr(ac3.get_config("addr")).get_snapshot().is_verified
|
||||||
|
|
||||||
logging.info("ac3: create a verified 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", protect=True)
|
||||||
vg.add_contact(ac3.create_contact(ac2))
|
vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr")))
|
||||||
|
|
||||||
# ensure ac2 receives message in VG
|
# ensure ac2 receives message in VG
|
||||||
vg.send_text("hello")
|
vg.send_text("hello")
|
||||||
while 1:
|
while 1:
|
||||||
msg = ac2.wait_for_incoming_msg().get_snapshot()
|
msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
if msg.text == "hello":
|
if msg.text == "hello":
|
||||||
|
assert msg.chat.get_basic_snapshot().is_protected
|
||||||
break
|
break
|
||||||
|
|
||||||
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
|
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
|
||||||
@@ -495,19 +436,19 @@ def test_qr_new_group_unblocked(acfactory):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
ac1, ac2 = acfactory.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", protect=True)
|
||||||
qr_code = ac1_chat.get_qr_code()
|
qr_code = ac1_chat.get_qr_code()
|
||||||
ac2.secure_join(qr_code)
|
ac2.secure_join(qr_code)
|
||||||
|
|
||||||
ac1.wait_for_securejoin_inviter_success()
|
ac1.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
ac1_new_chat = ac1.create_group("Another group")
|
ac1_new_chat = ac1.create_group("Another group")
|
||||||
ac1_new_chat.add_contact(ac1.create_contact(ac2))
|
ac1_new_chat.add_contact(ac1.get_contact_by_addr(ac2.get_config("addr")))
|
||||||
# Receive "Member added" message.
|
# Receive "Member added" message.
|
||||||
ac2.wait_for_incoming_msg_event()
|
ac2.wait_for_incoming_msg_event()
|
||||||
|
|
||||||
ac1_new_chat.send_text("Hello!")
|
ac1_new_chat.send_text("Hello!")
|
||||||
ac2_msg = ac2.wait_for_incoming_msg().get_snapshot()
|
ac2_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert ac2_msg.text == "Hello!"
|
assert ac2_msg.text == "Hello!"
|
||||||
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
|
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
|
||||||
|
|
||||||
@@ -520,7 +461,8 @@ def test_aeap_flow_verified(acfactory):
|
|||||||
addr, password = acfactory.get_credentials()
|
addr, password = acfactory.get_credentials()
|
||||||
|
|
||||||
logging.info("ac1: create verified-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", protect=True)
|
||||||
|
assert chat.get_basic_snapshot().is_protected
|
||||||
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")
|
||||||
ac2.secure_join(qr_code)
|
ac2.secure_join(qr_code)
|
||||||
@@ -532,7 +474,7 @@ def test_aeap_flow_verified(acfactory):
|
|||||||
|
|
||||||
logging.info("receiving first message")
|
logging.info("receiving first message")
|
||||||
ac2.wait_for_incoming_msg_event() # member added message
|
ac2.wait_for_incoming_msg_event() # member added message
|
||||||
msg_in_1 = ac2.wait_for_incoming_msg().get_snapshot()
|
msg_in_1 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
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")
|
||||||
@@ -546,7 +488,7 @@ def test_aeap_flow_verified(acfactory):
|
|||||||
msg_out = chat.send_text("changed address").get_snapshot()
|
msg_out = chat.send_text("changed address").get_snapshot()
|
||||||
|
|
||||||
logging.info("receiving second message")
|
logging.info("receiving second message")
|
||||||
msg_in_2 = ac2.wait_for_incoming_msg()
|
msg_in_2 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
|
||||||
msg_in_2_snapshot = msg_in_2.get_snapshot()
|
msg_in_2_snapshot = msg_in_2.get_snapshot()
|
||||||
assert msg_in_2_snapshot.text == msg_out.text
|
assert msg_in_2_snapshot.text == msg_out.text
|
||||||
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
|
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
|
||||||
@@ -574,35 +516,33 @@ def test_gossip_verification(acfactory) -> None:
|
|||||||
|
|
||||||
logging.info("Bob creates an Autocrypt group")
|
logging.info("Bob creates an Autocrypt group")
|
||||||
bob_group_chat = bob.create_group("Autocrypt Group")
|
bob_group_chat = bob.create_group("Autocrypt Group")
|
||||||
|
assert not bob_group_chat.get_basic_snapshot().is_protected
|
||||||
bob_group_chat.add_contact(bob_contact_alice)
|
bob_group_chat.add_contact(bob_contact_alice)
|
||||||
bob_group_chat.add_contact(bob_contact_carol)
|
bob_group_chat.add_contact(bob_contact_carol)
|
||||||
bob_group_chat.send_message(text="Hello Autocrypt group")
|
bob_group_chat.send_message(text="Hello Autocrypt group")
|
||||||
|
|
||||||
snapshot = carol.wait_for_incoming_msg().get_snapshot()
|
snapshot = carol.get_message_by_id(carol.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert snapshot.text == "Hello Autocrypt group"
|
assert snapshot.text == "Hello Autocrypt group"
|
||||||
assert snapshot.show_padlock
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
# Group propagates verification using Autocrypt-Gossip header.
|
# Autocrypt group does not propagate verification.
|
||||||
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
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
|
assert not carol_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
logging.info("Bob creates a Securejoin group")
|
logging.info("Bob creates a Securejoin group")
|
||||||
bob_group_chat = bob.create_group("Securejoin Group")
|
bob_group_chat = bob.create_group("Securejoin Group", protect=True)
|
||||||
|
assert bob_group_chat.get_basic_snapshot().is_protected
|
||||||
bob_group_chat.add_contact(bob_contact_alice)
|
bob_group_chat.add_contact(bob_contact_alice)
|
||||||
bob_group_chat.add_contact(bob_contact_carol)
|
bob_group_chat.add_contact(bob_contact_carol)
|
||||||
bob_group_chat.send_message(text="Hello Securejoin group")
|
bob_group_chat.send_message(text="Hello Securejoin group")
|
||||||
|
|
||||||
snapshot = carol.wait_for_incoming_msg().get_snapshot()
|
snapshot = carol.get_message_by_id(carol.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert snapshot.text == "Hello Securejoin group"
|
assert snapshot.text == "Hello Securejoin group"
|
||||||
assert snapshot.show_padlock
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
# Securejoin propagates verification.
|
# Securejoin propagates verification.
|
||||||
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||||
# Until we reset verifications and then send the _verified header,
|
assert carol_contact_alice_snapshot.is_verified
|
||||||
# verification is not gossiped here:
|
|
||||||
assert not carol_contact_alice_snapshot.is_verified
|
|
||||||
|
|
||||||
|
|
||||||
def test_securejoin_after_contact_resetup(acfactory) -> None:
|
def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||||
@@ -614,7 +554,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
|||||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
# ac3 creates protected group with ac1.
|
# ac3 creates protected group with ac1.
|
||||||
ac3_chat = ac3.create_group("Group")
|
ac3_chat = ac3.create_group("Verified group", protect=True)
|
||||||
|
|
||||||
# ac1 joins ac3 group.
|
# ac1 joins ac3 group.
|
||||||
ac3_qr_code = ac3_chat.get_qr_code()
|
ac3_qr_code = ac3_chat.get_qr_code()
|
||||||
@@ -622,7 +562,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
|||||||
ac1.wait_for_securejoin_joiner_success()
|
ac1.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
# 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.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
|
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()
|
||||||
|
|
||||||
@@ -637,8 +577,30 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
|||||||
|
|
||||||
# ac1 resetups the account.
|
# ac1 resetups the account.
|
||||||
ac1 = acfactory.resetup_account(ac1)
|
ac1 = acfactory.resetup_account(ac1)
|
||||||
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
|
||||||
assert not ac2_contact_ac1.get_snapshot().is_verified
|
# Loop sending message from ac1 to ac2
|
||||||
|
# until ac2 accepts new ac1 key.
|
||||||
|
#
|
||||||
|
# This may not happen immediately because resetup of ac1
|
||||||
|
# rewinds "smeared timestamp" so Date: header for messages
|
||||||
|
# sent by new ac1 are in the past compared to the last Date:
|
||||||
|
# header sent by old ac1.
|
||||||
|
while True:
|
||||||
|
# ac1 sends a message to ac2.
|
||||||
|
ac1_contact_ac2 = ac1.create_contact(ac2, "")
|
||||||
|
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
|
||||||
|
ac1_chat_ac2.send_text("Hello!")
|
||||||
|
|
||||||
|
# ac2 receives a message.
|
||||||
|
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hello!"
|
||||||
|
logging.info("ac2 received Hello!")
|
||||||
|
|
||||||
|
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
|
||||||
|
logging.info("ac2 addr={}, ac1 addr={}".format(ac2.get_config("addr"), ac1.get_config("addr")))
|
||||||
|
if not ac2_contact_ac1.get_snapshot().is_verified:
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
# ac1 goes offline.
|
# ac1 goes offline.
|
||||||
ac1.remove()
|
ac1.remove()
|
||||||
@@ -659,9 +621,10 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
|||||||
|
|
||||||
# Wait for member added.
|
# Wait for member added.
|
||||||
logging.info("ac2 waits for member added message")
|
logging.info("ac2 waits for member added message")
|
||||||
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
|
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert snapshot.is_info
|
assert snapshot.is_info
|
||||||
ac2_chat = snapshot.chat
|
ac2_chat = snapshot.chat
|
||||||
|
assert ac2_chat.get_basic_snapshot().is_protected
|
||||||
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.
|
# ac1 is still "not verified" for ac2 due to inconsistent state.
|
||||||
@@ -671,8 +634,9 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
|||||||
def test_withdraw_securejoin_qr(acfactory):
|
def test_withdraw_securejoin_qr(acfactory):
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
logging.info("Alice creates a group")
|
logging.info("Alice creates a verified group")
|
||||||
alice_chat = alice.create_group("Group")
|
alice_chat = alice.create_group("Verified group", protect=True)
|
||||||
|
assert alice_chat.get_basic_snapshot().is_protected
|
||||||
logging.info("Bob joins verified group")
|
logging.info("Bob joins verified group")
|
||||||
|
|
||||||
qr_code = alice_chat.get_qr_code()
|
qr_code = alice_chat.get_qr_code()
|
||||||
@@ -681,8 +645,9 @@ def test_withdraw_securejoin_qr(acfactory):
|
|||||||
|
|
||||||
alice.clear_all_events()
|
alice.clear_all_events()
|
||||||
|
|
||||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||||
|
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||||
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()
|
||||||
@@ -701,6 +666,6 @@ def test_withdraw_securejoin_qr(acfactory):
|
|||||||
event = alice.wait_for_event()
|
event = alice.wait_for_event()
|
||||||
if (
|
if (
|
||||||
event.kind == EventType.WARNING
|
event.kind == EventType.WARNING
|
||||||
and "Ignoring RequestWithAuth message because of invalid auth code." in event.msg
|
and "Ignoring vg-request-with-auth message because of invalid auth code." in event.msg
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,8 @@
|
|||||||
def test_vcard(acfactory) -> None:
|
def test_vcard(acfactory) -> None:
|
||||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob.create_chat(alice)
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
|
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
|
||||||
alice_contact_charlie_snapshot = alice_contact_charlie.get_snapshot()
|
|
||||||
alice_contact_fiona = alice.create_contact(fiona, "Fiona")
|
|
||||||
alice_contact_fiona_snapshot = alice_contact_fiona.get_snapshot()
|
|
||||||
|
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
alice_chat_bob.send_contact(alice_contact_charlie)
|
alice_chat_bob.send_contact(alice_contact_charlie)
|
||||||
@@ -16,12 +12,3 @@ def test_vcard(acfactory) -> None:
|
|||||||
snapshot = message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
assert snapshot.vcard_contact
|
assert snapshot.vcard_contact
|
||||||
assert snapshot.vcard_contact.addr == "charlie@example.org"
|
assert snapshot.vcard_contact.addr == "charlie@example.org"
|
||||||
assert snapshot.vcard_contact.color == alice_contact_charlie_snapshot.color
|
|
||||||
|
|
||||||
alice_chat_bob.send_contact(alice_contact_fiona)
|
|
||||||
event = bob.wait_for_incoming_msg_event()
|
|
||||||
message = bob.get_message_by_id(event.msg_id)
|
|
||||||
snapshot = message.get_snapshot()
|
|
||||||
assert snapshot.vcard_contact
|
|
||||||
assert snapshot.vcard_contact.key
|
|
||||||
assert snapshot.vcard_contact.color == alice_contact_fiona_snapshot.color
|
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ def test_webxdc(acfactory) -> None:
|
|||||||
"sourceCodeUrl": None,
|
"sourceCodeUrl": None,
|
||||||
"summary": None,
|
"summary": None,
|
||||||
"selfAddr": webxdc_info["selfAddr"],
|
"selfAddr": webxdc_info["selfAddr"],
|
||||||
"isAppSender": False,
|
|
||||||
"isBroadcast": False,
|
|
||||||
"sendUpdateInterval": 1000,
|
"sendUpdateInterval": 1000,
|
||||||
"sendUpdateMaxSize": 18874368,
|
"sendUpdateMaxSize": 18874368,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-rpc-server"
|
name = "deltachat-rpc-server"
|
||||||
version = "2.50.0-dev"
|
version = "1.159.3"
|
||||||
description = "DeltaChat JSON-RPC server"
|
description = "DeltaChat JSON-RPC server"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user