mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 15:42:10 +03:00
Compare commits
1 Commits
link2xt/ir
...
sk/old_bac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44e486693d |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -7,8 +7,6 @@ updates:
|
||||
commit-message:
|
||||
prefix: "chore(cargo)"
|
||||
open-pull-requests-limit: 50
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# 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>
|
||||
@@ -16,5 +14,3 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
46
.github/workflows/ci.yml
vendored
46
.github/workflows/ci.yml
vendored
@@ -20,7 +20,7 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.90.0
|
||||
RUST_VERSION: 1.87.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.85.0
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
name: cargo deny
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -67,12 +67,10 @@ jobs:
|
||||
name: Check provider database
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Install rustfmt
|
||||
run: rustup component add --toolchain stable-x86_64-unknown-linux-gnu rustfmt
|
||||
- name: Check provider database
|
||||
run: scripts/update-provider-database.sh
|
||||
|
||||
@@ -82,7 +80,7 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -117,7 +115,7 @@ jobs:
|
||||
shell: bash
|
||||
if: matrix.rust == 'latest'
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -139,12 +137,12 @@ jobs:
|
||||
- name: Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo nextest run --workspace --locked
|
||||
run: cargo nextest run --workspace
|
||||
|
||||
- name: Doc-Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo test --workspace --locked --doc
|
||||
run: cargo test --workspace --doc
|
||||
|
||||
- name: Test cargo vendor
|
||||
run: cargo vendor
|
||||
@@ -156,7 +154,7 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -181,7 +179,7 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -203,7 +201,7 @@ jobs:
|
||||
name: Python lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -228,9 +226,9 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.14
|
||||
python: 3.13
|
||||
- os: macos-latest
|
||||
python: 3.14
|
||||
python: 3.13
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -246,19 +244,19 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download libdeltachat.a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -281,11 +279,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python: 3.14
|
||||
python: 3.13
|
||||
- os: macos-latest
|
||||
python: 3.14
|
||||
python: 3.13
|
||||
- os: windows-latest
|
||||
python: 3.14
|
||||
python: 3.13
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -299,13 +297,13 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -313,7 +311,7 @@ jobs:
|
||||
run: pip install tox
|
||||
|
||||
- name: Download deltachat-rpc-server
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: target/debug
|
||||
|
||||
70
.github/workflows/deltachat-rpc-server.yml
vendored
70
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -30,11 +30,11 @@ jobs:
|
||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
@@ -54,11 +54,11 @@ jobs:
|
||||
arch: [win32, win64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -105,11 +105,11 @@ jobs:
|
||||
arch: [arm64-v8a, armeabi-v7a]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
@@ -132,74 +132,74 @@ jobs:
|
||||
contents: write
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-macos
|
||||
path: deltachat-rpc-server-x86_64-macos.d
|
||||
|
||||
- name: Download macOS binary for aarch64
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-macos
|
||||
path: deltachat-rpc-server-aarch64-macos.d
|
||||
|
||||
- name: Download Android binary for arm64-v8a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-arm64-v8a-android
|
||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||
|
||||
- name: Download Android binary for armeabi-v7a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android
|
||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||
@@ -224,7 +224,7 @@ jobs:
|
||||
|
||||
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
|
||||
- name: Install python 3.12
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
@@ -285,76 +285,76 @@ jobs:
|
||||
# Needed to publish the binaries to the release.
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-macos
|
||||
path: deltachat-rpc-server-x86_64-macos.d
|
||||
|
||||
- name: Download macOS binary for aarch64
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-macos
|
||||
path: deltachat-rpc-server-aarch64-macos.d
|
||||
|
||||
- name: Download Android binary for arm64-v8a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-arm64-v8a-android
|
||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||
|
||||
- name: Download Android binary for armeabi-v7a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android
|
||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||
@@ -401,7 +401,7 @@ jobs:
|
||||
deltachat-rpc-server/npm-package/*.tgz
|
||||
|
||||
# Configure Node.js for publishing.
|
||||
- uses: actions/setup-node@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
@@ -14,12 +14,12 @@ jobs:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
4
.github/workflows/jsonrpc.yml
vendored
4
.github/workflows/jsonrpc.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
||||
build_and_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Add Rust cache
|
||||
|
||||
24
.github/workflows/nix.yml
vendored
24
.github/workflows/nix.yml
vendored
@@ -5,12 +5,10 @@ on:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
- .github/workflows/nix.yml
|
||||
push:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
- .github/workflows/nix.yml
|
||||
branches:
|
||||
- main
|
||||
|
||||
@@ -21,12 +19,15 @@ jobs:
|
||||
name: check flake formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- run: nix fmt flake.nix -- --check
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- run: nix fmt
|
||||
|
||||
# Check that formatting does not change anything.
|
||||
- run: git diff --exit-code
|
||||
|
||||
build:
|
||||
name: nix build
|
||||
@@ -80,11 +81,11 @@ jobs:
|
||||
#- deltachat-rpc-server-x86_64-android
|
||||
#- deltachat-rpc-server-x86-android
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -95,15 +96,14 @@ jobs:
|
||||
matrix:
|
||||
installable:
|
||||
- deltachat-rpc-server
|
||||
- deltachat-rpc-server-x86_64-darwin
|
||||
|
||||
# Fails to build
|
||||
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
|
||||
# Fails to bulid
|
||||
# - deltachat-rpc-server-aarch64-darwin
|
||||
# - deltachat-rpc-server-x86_64-darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
|
||||
4
.github/workflows/repl.yml
vendored
4
.github/workflows/repl.yml
vendored
@@ -14,11 +14,11 @@ jobs:
|
||||
name: Build REPL example
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
|
||||
14
.github/workflows/upload-docs.yml
vendored
14
.github/workflows/upload-docs.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -31,12 +31,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
@@ -50,12 +50,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
@@ -72,13 +72,13 @@ jobs:
|
||||
working-directory: ./deltachat-jsonrpc/typescript
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: npm install
|
||||
|
||||
2
.github/workflows/upload-ffi-docs.yml
vendored
2
.github/workflows/upload-ffi-docs.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
6
.github/workflows/zizmor-scan.yml
vendored
6
.github/workflows/zizmor-scan.yml
vendored
@@ -14,18 +14,18 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Run zizmor
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -36,7 +36,6 @@ deltachat-ffi/xml
|
||||
coverage/
|
||||
.DS_Store
|
||||
.vscode
|
||||
.zed
|
||||
python/accounts.txt
|
||||
python/all-testaccounts.txt
|
||||
tmp/
|
||||
|
||||
626
CHANGELOG.md
626
CHANGELOG.md
@@ -1,594 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [2.22.0] - 2025-10-17
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not notify about incoming calls for contact requests and blocked contacts.
|
||||
|
||||
### Tests
|
||||
|
||||
- Accept the chat with the caller before accepting calls.
|
||||
|
||||
## [2.21.0] - 2025-10-16
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: Remove unused dependencies.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- TLS 1.3 session resumption.
|
||||
- REPL: Add send-sync command.
|
||||
- Set `User-Agent` for tile.openstreetmap.org requests.
|
||||
- Cache tile.openstreetmap.org tiles for 7 days.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Remove Exif with non-fatal errors from images.
|
||||
- jsonrpc: Use Core's logic for computing VcardContact.color ([#7294](https://github.com/chatmail/core/pull/7294)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- deps: Bump cachix/install-nix-action from 31.7.0 to 31.8.0.
|
||||
- cargo: Bump async_zip from 0.0.17 to 0.0.18 ([#7257](https://github.com/chatmail/core/pull/7257)).
|
||||
- deps: Bump github/codeql-action from 3 to 4 ([#7304](https://github.com/chatmail/core/pull/7304)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use rustls reexported from tokio_rustls.
|
||||
- Pass ALPN around as &str.
|
||||
- mimeparser: Store only one signature fingerprint.
|
||||
|
||||
### Tests
|
||||
|
||||
- Test expiration of ephemeral messages with unknown viewtype.
|
||||
- Test expiration of non-ephemeral message with unknown viewtype.
|
||||
|
||||
## [2.20.0] - 2025-10-13
|
||||
|
||||
This release fixes a bug that resulted in ephemeral loop getting stuck in infinite loop
|
||||
when trying to delete a message with unknown viewtype.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Accept unknown viewtype in ephemeral loop.
|
||||
- Accept unknown viewtype in delete-old-messages loop.
|
||||
|
||||
## [2.19.0] - 2025-10-12
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Slightly increase saturation of colors.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not fail to receive call accepted/ended messages referring to non-call Message-ID.
|
||||
- Do not fail to fully download previously trashed messages.
|
||||
- Emit AccountsItemChanged when own key is generated/imported, use gray self-color until that ([#7296](https://github.com/chatmail/core/pull/7296)).
|
||||
- Do not try to process calls from partial messages.
|
||||
|
||||
### CI
|
||||
|
||||
- Update to Python 3.14.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use variables directly in formatted strings ([#7284](https://github.com/chatmail/core/pull/7284)).
|
||||
- Set_chat_profile_image(): Remove !chat.is_mailing_list() check.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump quick-xml from 0.37.5 to 0.38.3.
|
||||
- Add nodejs to nix dev env ([#7283](https://github.com/chatmail/core/pull/7283))
|
||||
|
||||
## [2.18.0] - 2025-10-08
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove APIs for video chat invitations.
|
||||
|
||||
### CI
|
||||
|
||||
- nix: Run the workflow when workflow file changes.
|
||||
- nix: Switch from DeterminateSystems/nix-installer-action to cachix/install-nix-action.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- No implicit member changes from old Delta Chat clients ([#7220](https://github.com/chatmail/core/pull/7220)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not fail to load messages with unknown viewtype.
|
||||
- Only omit group changes messages if SELF is really added ([#7220](https://github.com/chatmail/core/pull/7220)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Assert that Iroh node addresses have home relay URL.
|
||||
|
||||
## [2.17.0] - 2025-10-04
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove deprecated verified_one_on_one_chats config.
|
||||
|
||||
### CI
|
||||
|
||||
- Require that Cargo.lock is up to date.
|
||||
- Fix CI checking Nix formatting.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Comment about outdated timespan.
|
||||
- Clarify CALL events ([#7188](https://github.com/chatmail/core/pull/7188)).
|
||||
- Add docs for JS `BaseDeltaChat`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Make `text/calendar` alternative available as an attachment.
|
||||
- Better summary for calls.
|
||||
- Add strings 'You left the channel.' and 'Scan to join Channel' ([#7266](https://github.com/chatmail/core/pull/7266)).
|
||||
- Stock strings for calls.
|
||||
- ffi: Add DC_STR_CANT_DECRYPT_OUTGOING_MSGS define.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Prefer last part in `multipart/alternative`.
|
||||
- Prefetch messages in limited batches ([#6915](https://github.com/chatmail/core/pull/6915)).
|
||||
- Forward calls as text messages.
|
||||
- Consistent spelling of "canceled" with a single "l".
|
||||
- Lowercase "call" in "Missed call" and similar strings.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Return the reason when failing to place calls.
|
||||
|
||||
### Tests
|
||||
|
||||
- Test reception of `multipart/alternative` with `text/calendar`.
|
||||
|
||||
## [2.16.0] - 2025-10-01
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Get rid of inviter progress other than 0 and 1000.
|
||||
- Add has_video attribute to incoming call events.
|
||||
- Add JSON-RPC API to get ICE servers.
|
||||
- Add call_info() JSON-RPC API.
|
||||
- Add chat ID to SecureJoinInviterProgress.
|
||||
- deltachat-rpc-client: Add Chat.resend_messages().
|
||||
- Add `chat_id` to all call events ([#7216](https://github.com/chatmail/core/pull/7216)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Update rPGP from 0.16.0 to 0.17.0.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.90.0.
|
||||
- Install rustfmt before checking provider database.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add more `get_next_event` docs.
|
||||
- SecurejoinInviterProgress never returns an error.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Don't fetch messages from unknown folders ([#7190](https://github.com/chatmail/core/pull/7190)).
|
||||
- Get ICE servers from IMAP METADATA.
|
||||
- Don't ignore receive_imf_inner() errors, try adding partially downloaded message instead ([#7196](https://github.com/chatmail/core/pull/7196)).
|
||||
- Set dimensions for outgoing Sticker messages.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Create 1:1 chat only if auth token is for setup contact.
|
||||
- Ignore vc-/vg- prefix for SecurejoinInviterProgress.
|
||||
- Don't init Iroh on channel leave ([#7210](https://github.com/chatmail/core/pull/7210)).
|
||||
- Take the last valid Autocrypt header ([#7167](https://github.com/chatmail/core/pull/7167)).
|
||||
- Don't add "member removed" messages from nonmembers ([#7207](https://github.com/chatmail/core/pull/7207)).
|
||||
- Do not consider the call stale if it is not sent out yet.
|
||||
- Receive_imf: Report replaced message id in `MsgsChanged` if chat is the same.
|
||||
- Allow Exif for stickers, don't recode them because of that ([#6447](https://github.com/chatmail/core/pull/6447)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove unused prop (TS, `BaseDeltaChat`).
|
||||
- Remove unused FolderMeaning::Drafts.
|
||||
|
||||
### Tests
|
||||
|
||||
- Rename test_udpate_call_text into test_update_call_text.
|
||||
- Update timestamp_sent in pop_sent_msg_opt().
|
||||
- Do not match call ID from second alice with first alice event.
|
||||
|
||||
## [2.15.0] - 2025-09-15
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add JSON-RPC API for calls ([#7194](https://github.com/chatmail/core/pull/7194)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Remove unused `quoted_printable` dependency.
|
||||
|
||||
## [2.14.0] - 2025-09-12
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Put the chattype into the SecurejoinInviterProgress event ([#7181](https://github.com/chatmail/core/pull/7181)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- param: Split params only on \n.
|
||||
- B-encode SDP offer and answer sent in headers.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use recv_msg_trash() instead of recv_msg_opt().
|
||||
- Prepare_msg_raw(): don't return MsgId.
|
||||
|
||||
### Tests
|
||||
|
||||
- Message is OutFailed if all keys are missing ([#6849](https://github.com/chatmail/core/pull/6849)).
|
||||
- Test sending SDP offer and answer with newlines.
|
||||
|
||||
## [2.13.0] - 2025-09-09
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove `is_profile_verified` APIs.
|
||||
- [**breaking**] Remove deprecated `is_protection_broken`.
|
||||
- [**breaking**] Remove `e2ee_enabled` preference.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add call ringing API ([#6650](https://github.com/chatmail/core/pull/6650), [#7174](https://github.com/chatmail/core/pull/7174), [#7175](https://github.com/chatmail/core/pull/7175), [#7179](https://github.com/chatmail/core/pull/7179))
|
||||
- Warn for outdated versions after 6 months instead of 1 year ([#7144](https://github.com/chatmail/core/pull/7144)).
|
||||
- Do not set "unknown sender for this chat" error.
|
||||
- Do not replace messages with an error on verification failure.
|
||||
- Support receiving Autocrypt-Gossip with `_verified` attribute.
|
||||
- Withdraw all QR codes when one is withdrawn.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't reverify contacts by SELF on receipt of a message from another device.
|
||||
- Don't verify contacts by others having an unknown verifier.
|
||||
- Update verifier_id if it's "unknown" and new verifier has known verifier.
|
||||
- Mark message as failed if it can't be sent ([#7143](https://github.com/chatmail/core/pull/7143)).
|
||||
- Add "Messages are end-to-end encrypted." to non-protected groups.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix for SecurejoinInviterProgress with progress == 600.
|
||||
- STYLE.md: Prefer BTreeMap and BTreeSet over hash variants.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update provider database.
|
||||
- Update dependencies.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Check that verifier is verified in turn.
|
||||
- Remove unused `EncryptPreference::Reset`.
|
||||
- Remove `Aheader::new`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Add another TimeShiftFalsePositiveNote ([#7142](https://github.com/chatmail/core/pull/7142)).
|
||||
- Add TestContext.create_chat_id.
|
||||
|
||||
## [2.12.0] - 2025-08-26
|
||||
|
||||
### API-Changes
|
||||
|
||||
- api!(python): remove remaining broken API for reactions
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Use Group ID for chat color generation instead of the name for encrypted groups.
|
||||
- Use key fingerprints instead of addresses for key-contacts color generation.
|
||||
- Replace HSLuv colors with OKLCh.
|
||||
- `wal_checkpoint()`: Do `wal_checkpoint(PASSIVE)` and `wal_checkpoint(FULL)` before `wal_checkpoint(TRUNCATE)`.
|
||||
- Assign messages to key-contacts based on Issuer Fingerprint.
|
||||
- Create_group_ex(): Log and replace invalid chat name with "…".
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not create a group if the sender includes self in the `To` field.
|
||||
- Do not reverify already verified contacts via gossip.
|
||||
- `get_connectivity()`: Get rid of locking SchedulerState::inner ([#7124](https://github.com/chatmail/core/pull/7124)).
|
||||
- Make reaction message hidden only if there are no other parts.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Do not return `Result` from `valid_signature_fingerprints()`.
|
||||
- Make `ConnectivityStore` use a non-async lock ([#7129](https://github.com/chatmail/core/pull/7129)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Remove broken link from documentation comments.
|
||||
- Remove the comment about Color Vision Deficiency correction.
|
||||
|
||||
## [2.11.0] - 2025-08-13
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Contact::lookup_id_by_addr_ex: Prefer returning key-contact.
|
||||
- Contact::lookup_id_by_addr_ex: Prefer returning accepted contacts.
|
||||
- Better string when using disappearing messages of one year (365..367 days, so it can be tweaked later).
|
||||
- Do not require resent messages to be from the same chat.
|
||||
- `lookup_key_contact_by_address()`: Allow looking up ContactId::SELF without chat id.
|
||||
- `get_securejoin_qr()`: Log error if group doesn't have grpid.
|
||||
- `receive_imf::add_parts()`: Get rid of extra `Chat::load_from_db()` calls.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Ignore case when trying to detect 'invalid unencrypted mail' and add an info-message.
|
||||
- Run wal_checkpoint during housekeeping ([#6089](https://github.com/chatmail/core/pull/6089)).
|
||||
- Allow receiving empty files.
|
||||
- Set correct sent_timestamp for saved outgoing messages.
|
||||
- Do not remove query parameters from URLs.
|
||||
- Log and set imex progress error ([#7091](https://github.com/chatmail/core/pull/7091)).
|
||||
- Do not add key-contacts to unencrypted groups.
|
||||
- Do not reset `GuaranteeE2ee` in the database when resending messages.
|
||||
- Assign messages to a group if there is a `Chat-Group-Name`.
|
||||
- Take `Chat-Group-Name` into account when matching ad hoc groups.
|
||||
- Don't break long group names with non-ASCII characters.
|
||||
- Add messages that can't be verified as `DownloadState::Available` ([#7059](https://github.com/chatmail/core/pull/7059)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Log the number of the test account if there are multiple alices ([#7087](https://github.com/chatmail/core/pull/7087)).
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.89.0.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Rename icon-address-contact to icon-unencrypted.
|
||||
- Skip loading the contact of 1:1 unencrypted chat to show the avatar.
|
||||
- Chat::is_encrypted(): Make one query instead of two for 1:1 chats.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump toml from 0.8.23 to 0.9.4.
|
||||
- cargo: Bump human-panic from 2.0.2 to 2.0.3.
|
||||
- deny.toml: Add exception for duplicate toml_datetime 0.6.11 dependency.
|
||||
- deps: Bump actions/checkout from 4 to 5.
|
||||
- deps: Bump actions/download-artifact from 4 to 5.
|
||||
|
||||
## [2.10.0] - 2025-08-04
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Also lookup key contacts in lookup_id_by_addr() ([#7073](https://github.com/chatmail/core/pull/7073)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump serde_json from 1.0.140 to 1.0.142.
|
||||
- cargo: Bump bolero from 0.13.3 to 0.13.4.
|
||||
- cargo: Bump async-channel from 2.3.1 to 2.5.0.
|
||||
- cargo: Bump hyper-util from 0.1.14 to 0.1.16.
|
||||
- cargo: Bump criterion from 0.6.0 to 0.7.0.
|
||||
- cargo: Bump strum from 0.27.1 to 0.27.2.
|
||||
- cargo: Bump strum_macros from 0.27.1 to 0.27.2.
|
||||
- Upgrade async-imap to 0.11.1.
|
||||
|
||||
## [2.9.0] - 2025-07-31
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- repl: Add import-vcard and make-vcard commands ([#7048](https://github.com/chatmail/core/pull/7048)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Display correct timer value for ephemeral timer changes.
|
||||
- Get_chat_msgs_ex(): Report local midnight in ChatItem::DayMarker.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Rename add_or_lookup_key_contacts_by_address_list() to add_or_lookup_key_contacts().
|
||||
- Don't call add_or_lookup_key_contacts() in advance.
|
||||
|
||||
## [2.8.0] - 2025-07-28
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Remove ProtectionBroken, make such chats Unprotected ([#7041](https://github.com/chatmail/core/pull/7041)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Lookup self by address if there is no fingerprint or gossip.
|
||||
|
||||
## [2.7.0] - 2025-07-26
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Mimefactory: Order message recipients by time of addition ([#6872](https://github.com/chatmail/core/pull/6872)).
|
||||
- Put the debug/release build version into the info ([#7034](https://github.com/chatmail/core/pull/7034)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Realtime late join ([#6869](https://github.com/chatmail/core/pull/6869)).
|
||||
- Do not fail to upgrade if the verifier of a contact doesn't exist anymore ([#7044](https://github.com/chatmail/core/pull/7044)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Add regression test for verification-gossiping crash ([#7033](https://github.com/chatmail/core/pull/7033)).
|
||||
|
||||
## [2.6.0] - 2025-07-23
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix crash when receiving a verification-gossiping message which a contact also sends to itself ([#7032](https://github.com/chatmail/core/pull/7032)).
|
||||
|
||||
## [2.5.0] - 2025-07-22
|
||||
|
||||
### Fixes
|
||||
|
||||
- Correctly migrate "verified by me".
|
||||
- Mark all email chats as unprotected in the migration ([#7026](https://github.com/chatmail/core/pull/7026)).
|
||||
- Do not ignore errors in add_flag_finalized_with_set.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Deprecate protection-broken and related stuff ([#7018](https://github.com/chatmail/core/pull/7018)).
|
||||
- Clarify the meaning of is_verified() vs verifier_id() ([#7027](https://github.com/chatmail/core/pull/7027)).
|
||||
- STYLE.md: Prefer `try_next()` over `next()`.
|
||||
|
||||
## [2.4.0] - 2025-07-21
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not ignore errors when draining FETCH responses. This avoids IMAP loop getting stuck in an infinite loop retrying reading from the connection.
|
||||
- Update `tokio-io-timeout` to 1.2.1. This release includes a fix to reset timeout after every error, so timeout error is returned at most once a minute if read is attempted after a timeout.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update async-imap to 0.11.0.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use `try_next()` when processing FETCH responses.
|
||||
|
||||
## [2.3.0] - 2025-07-19
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add "e2ee encrypted" info message to all e2ee chats ([#7008](https://github.com/chatmail/core/pull/7008)).
|
||||
- repl: Print errors and debug logs to stderr.
|
||||
- `{ensure_and,logged}_debug_assert`: Don't evaluate condition twice.
|
||||
- Log when background fetch of all accounts finishes successfully.
|
||||
- Log the number of read/written bytes on IMAP stream read error ([#6924](https://github.com/chatmail/core/pull/6924)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Ignore protected headers in outer message part ([#6357](https://github.com/chatmail/core/pull/6357)).
|
||||
- List e-mail contacts in repl listcontacts command.
|
||||
- Save peer address for LoggingStream early.
|
||||
|
||||
## [2.2.0] - 2025-07-14
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add chat::create_group_ex(), deprecate create_group_chat() ([#6927](https://github.com/chatmail/core/pull/6927)).
|
||||
- jsonrpc: Add CommandApi::create_group_chat_unencrypted() ([#6927](https://github.com/chatmail/core/pull/6927)).
|
||||
- [**breaking**] In ChatListItem, replace is_group and is_(out_)broadcast with chat_type property ([#7003](https://github.com/chatmail/core/pull/7003)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Log failed debug assertions in all configurations.
|
||||
- Donation request device message ([#6913](https://github.com/chatmail/core/pull/6913)).
|
||||
- Advance next UID even if connection fails while fetching.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Always prefer the last header.
|
||||
|
||||
### Tests
|
||||
|
||||
- Tune down DELTACHAT_SAVE_TMP_DB hint ([#6998](https://github.com/chatmail/core/pull/6998)).
|
||||
- Unencrypted group creation ([#6927](https://github.com/chatmail/core/pull/6927)).
|
||||
|
||||
## [2.1.0] - 2025-07-11
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add account ordering functionality ([#6993](https://github.com/chatmail/core/pull/6993)).
|
||||
- feat: Make it possible to leave broadcast channels ([#6984](https://github.com/chatmail/core/pull/6984))
|
||||
- Migrations: Use tools::Time to measure time for logging.
|
||||
- Log emitted logging events with `tracing`.
|
||||
- Ensure_and_debug_assert{,_eq,_ne} macros combining `debug_assert*` and anyhow::ensure ([#6907](https://github.com/chatmail/core/pull/6907)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Use Viewtype::File for messages with invalid images, images of unknown size, images > 50 Mpx ([#6825](https://github.com/chatmail/core/pull/6825)).
|
||||
- Don't apply chat name and avatar changes from non-members.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update showpadlock ffi.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Update cordyceps from 0.3.2 to 0.3.4.
|
||||
|
||||
### Tests
|
||||
|
||||
- Add option to save database on test failure ([#6992](https://github.com/chatmail/core/pull/6992)).
|
||||
|
||||
## [2.0.0] - 2025-07-09
|
||||
|
||||
This release changes the way the core handles contact keys.
|
||||
Instead of tracking OpenPGP keys corresponding to the
|
||||
contacts in [Autocrypt](https://autocrypt.org/) peerstate,
|
||||
the core creates a new "key-contact" for each known public key.
|
||||
Reception of a message signed with a new unknown key
|
||||
no longer results in warnings about setup changes,
|
||||
but creates a new contact and a new 1:1 chat if necessary.
|
||||
Additionally, there are "address-contacts" corresponding
|
||||
to the e-mail addresses.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Key-contacts ([#6796](https://github.com/chatmail/core/pull/6796), [#6941](https://github.com/chatmail/core/pull/6941)).
|
||||
- Increase event channel size from 1000 to 10000.
|
||||
- Minimize the amount of data preserved for trashed messages.
|
||||
- Show broadcast channels in their own, proper "Channel" chat ([#6901](https://github.com/chatmail/core/pull/6901), [#6975](https://github.com/chatmail/core/pull/6975)).
|
||||
- Check images passed as `File` before making them `Image`.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- CFFI: Add dc_contact_is_key_contact() ([#6955](https://github.com/chatmail/core/pull/6955)).
|
||||
- Contact::get_all(): Support listing address-contacts.
|
||||
- [**breaking**] Add InBroadcastChannel, OutBroadcastChannel chattypes, add create_broadcast_channel() ([#6901](https://github.com/chatmail/core/pull/6901)).
|
||||
- deltachat-rpc-client: Add Message.get_read_receipts().
|
||||
|
||||
### Fixes
|
||||
|
||||
- Remove display name from get_info(). This information usually goes at the top of the log and we don't want users to include it in bug reports.
|
||||
- Wait for scheduler tasks shutdown in parallel.
|
||||
- Update deltachat-repl help and autocomplete to match implementation ([#6978](https://github.com/chatmail/core/pull/6978), ([#6979](https://github.com/chatmail/core/pull/6979)).
|
||||
- Send Autocrypt header in MDNs. This is needed to assign MDNs to key-contacts.
|
||||
- Prefer encrypted List-Id header ([#6983](https://github.com/chatmail/core/pull/6983)).
|
||||
- Treat "tgs" as Viewtype::File.
|
||||
- Treat and send images that can't be decoded as Viewtype::File.
|
||||
- Decide on filename used for sending depending on the original Viewtype.
|
||||
- Migrate_key_contacts(): Remove "id>9" from encrypted messages SELECT.
|
||||
- Save msgs to key-contacts migration state and run migration periodically ([#6956](https://github.com/chatmail/core/pull/6956)).
|
||||
- Do not try to lookup key-contacts for unencrypted 1:1 messages.
|
||||
- Add query to post request for account creation ([#6989](https://github.com/chatmail/core/pull/6989)).
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.88.0.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Remove outdated comment that says MDNs are unencrypted.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Upgrade to Rust 2024.
|
||||
- Build_body_file(): Remove guessing mimetype by file extension.
|
||||
|
||||
### Tests
|
||||
|
||||
- Add online test for read receipts.
|
||||
- Add a test reproducing chat assignment bug.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump smallvec from 1.15.0 to 1.15.1.
|
||||
- cargo: Bump syn from 2.0.101 to 2.0.104.
|
||||
- cargo: Bump hyper-util from 0.1.13 to 0.1.14.
|
||||
- cargo: Bump toml from 0.8.19 to 0.8.23.
|
||||
- cargo: Bump proptest from 1.6.0 to 1.7.0.
|
||||
- cargo: Bump libc from 0.2.172 to 0.2.174.
|
||||
|
||||
## [1.160.0] - 2025-06-22
|
||||
|
||||
### API-Changes
|
||||
@@ -878,8 +289,8 @@ to the e-mail addresses.
|
||||
|
||||
- Use vCard in TestContext.add_or_lookup_contact().
|
||||
- Remove test_group_with_removed_message_id.
|
||||
- Use add_or_lookup_address_contact() in get_chat().
|
||||
- Use add_or_lookup_address_contact in test_setup_contact_ex.
|
||||
- Use add_or_lookup_email_contact() in get_chat().
|
||||
- Use add_or_lookup_email_contact in test_setup_contact_ex.
|
||||
- Use vCards more in Python tests.
|
||||
- Use TestContextManager in more tests.
|
||||
- Use vCards to create contacts in more Rust tests.
|
||||
@@ -1911,7 +1322,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
|
||||
### Fixes
|
||||
|
||||
- Reset quota on configured address change ([#5908](https://github.com/chatmail/core/pull/5908)).
|
||||
- Do not emit progress 1000 when configuration is canceled.
|
||||
- Do not emit progress 1000 when configuration is cancelled.
|
||||
- Assume file extensions are 32 chars max and don't contain whitespace ([#5338](https://github.com/chatmail/core/pull/5338)).
|
||||
- Re-add tokens.foreign_id column ([#6038](https://github.com/chatmail/core/pull/6038)).
|
||||
|
||||
@@ -4359,7 +3770,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
|
||||
- Recreate `smtp` table with AUTOINCREMENT `id` ([#4390](https://github.com/chatmail/core/pull/4390)).
|
||||
- Do not return an error from `send_msg_to_smtp` if retry limit is exceeded.
|
||||
- Make the bots automatically accept group chat contact requests ([#4377](https://github.com/chatmail/core/pull/4377)).
|
||||
- Delete `smtp` rows when message sending is canceled ([#4391](https://github.com/chatmail/core/pull/4391)).
|
||||
- Delete `smtp` rows when message sending is cancelled ([#4391](https://github.com/chatmail/core/pull/4391)).
|
||||
|
||||
### Refactor
|
||||
|
||||
@@ -4370,7 +3781,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
|
||||
### Fixes
|
||||
|
||||
- Fetch at most 100 existing messages even if EXISTS was not received.
|
||||
- Delete `smtp` rows when message sending is canceled.
|
||||
- Delete `smtp` rows when message sending is cancelled.
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -4457,14 +3868,14 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
|
||||
## [1.112.3] - 2023-03-30
|
||||
|
||||
### Fixes
|
||||
- `transfer::get_backup` now frees ongoing process when canceled. #4249
|
||||
- `transfer::get_backup` now frees ongoing process when cancelled. #4249
|
||||
|
||||
## [1.112.2] - 2023-03-30
|
||||
|
||||
### Changes
|
||||
- Update iroh, remove `default-net` from `[patch.crates-io]` section.
|
||||
- transfer backup: Connect to multiple provider addresses concurrently. This should speed up connection time significantly on the getter side. #4240
|
||||
- Make sure BackupProvider is canceled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242
|
||||
- Make sure BackupProvider is cancelled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242
|
||||
|
||||
### Fixes
|
||||
- Do not return media from trashed messages in the "All media" view. #4247
|
||||
@@ -6945,26 +6356,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.159.4]: https://github.com/chatmail/core/compare/v1.159.3..v1.159.4
|
||||
[1.159.5]: https://github.com/chatmail/core/compare/v1.159.4..v1.159.5
|
||||
[1.160.0]: https://github.com/chatmail/core/compare/v1.159.5..v1.160.0
|
||||
[2.0.0]: https://github.com/chatmail/core/compare/v1.160.0..v2.0.0
|
||||
[2.1.0]: https://github.com/chatmail/core/compare/v2.0.0..v2.1.0
|
||||
[2.2.0]: https://github.com/chatmail/core/compare/v2.1.0..v2.2.0
|
||||
[2.3.0]: https://github.com/chatmail/core/compare/v2.2.0..v2.3.0
|
||||
[2.4.0]: https://github.com/chatmail/core/compare/v2.3.0..v2.4.0
|
||||
[2.5.0]: https://github.com/chatmail/core/compare/v2.4.0..v2.5.0
|
||||
[2.6.0]: https://github.com/chatmail/core/compare/v2.5.0..v2.6.0
|
||||
[2.7.0]: https://github.com/chatmail/core/compare/v2.6.0..v2.7.0
|
||||
[2.8.0]: https://github.com/chatmail/core/compare/v2.7.0..v2.8.0
|
||||
[2.9.0]: https://github.com/chatmail/core/compare/v2.8.0..v2.9.0
|
||||
[2.10.0]: https://github.com/chatmail/core/compare/v2.9.0..v2.10.0
|
||||
[2.11.0]: https://github.com/chatmail/core/compare/v2.10.0..v2.11.0
|
||||
[2.12.0]: https://github.com/chatmail/core/compare/v2.11.0..v2.12.0
|
||||
[2.13.0]: https://github.com/chatmail/core/compare/v2.12.0..v2.13.0
|
||||
[2.14.0]: https://github.com/chatmail/core/compare/v2.13.0..v2.14.0
|
||||
[2.15.0]: https://github.com/chatmail/core/compare/v2.14.0..v2.15.0
|
||||
[2.16.0]: https://github.com/chatmail/core/compare/v2.15.0..v2.16.0
|
||||
[2.17.0]: https://github.com/chatmail/core/compare/v2.16.0..v2.17.0
|
||||
[2.18.0]: https://github.com/chatmail/core/compare/v2.17.0..v2.18.0
|
||||
[2.19.0]: https://github.com/chatmail/core/compare/v2.18.0..v2.19.0
|
||||
[2.20.0]: https://github.com/chatmail/core/compare/v2.19.0..v2.20.0
|
||||
[2.21.0]: https://github.com/chatmail/core/compare/v2.20.0..v2.21.0
|
||||
[2.22.0]: https://github.com/chatmail/core/compare/v2.21.0..v2.22.0
|
||||
|
||||
@@ -44,7 +44,7 @@ If you want to contribute a code, follow this guide.
|
||||
|
||||
The following prefix types are used:
|
||||
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
|
||||
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is 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)`"
|
||||
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
|
||||
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
|
||||
|
||||
2475
Cargo.lock
generated
2475
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
54
Cargo.toml
54
Cargo.toml
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.22.0"
|
||||
edition = "2024"
|
||||
version = "1.160.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.85"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
@@ -44,16 +44,14 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
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.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
base64 = { workspace = true }
|
||||
blake3 = "1.8.2"
|
||||
brotli = { version = "8", default-features=false, features = ["std"] }
|
||||
bytes = "1"
|
||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
colorutils-rs = { version = "0.7.5", default-features = false }
|
||||
data-encoding = "2.9.0"
|
||||
escaper = "0.1"
|
||||
fast-socks5 = "0.10"
|
||||
@@ -61,16 +59,17 @@ fd-lock = "4"
|
||||
futures-lite = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "0.25.2"
|
||||
http-body-util = "0.1.3"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.16"
|
||||
hyper-util = "0.1.13"
|
||||
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh-gossip = { version = "0.94", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.94", default-features = false }
|
||||
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.35", default-features = false }
|
||||
kamadak-exif = "0.6.1"
|
||||
libc = { workspace = true }
|
||||
mail-builder = { version = "0.4.4", default-features = false }
|
||||
mail-builder = { version = "0.4.3", default-features = false }
|
||||
mailparse = { workspace = true }
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.17"
|
||||
@@ -78,44 +77,45 @@ num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.17.0", default-features = false }
|
||||
pgp = { version = "0.16.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = { version = "0.38", features = ["escape-html"] }
|
||||
rand-old = { package = "rand", version = "0.8" }
|
||||
quick-xml = "0.37"
|
||||
quoted_printable = "0.5"
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustls-pki-types = "1.12.0"
|
||||
rustls = { version = "0.23.22", default-features = false }
|
||||
sanitize-filename = { workspace = true }
|
||||
sdp = "0.8.0"
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = "0.7.1"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
||||
smallvec = "1.15.1"
|
||||
smallvec = "1.15.0"
|
||||
strum = "0.27"
|
||||
strum_macros = "0.27"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.2"
|
||||
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-stream = { version = "0.1.17", features = ["fs"] }
|
||||
astral-tokio-tar = { version = "0.5.6", default-features = false }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.9"
|
||||
tracing = "0.1.41"
|
||||
toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
webpki-roots = "0.26.8"
|
||||
blake3 = "1.8.2"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||
criterion = { version = "0.6.0", features = ["async_tokio"] }
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
@@ -174,29 +174,29 @@ harness = false
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
async-channel = "2.5.0"
|
||||
async-channel = "2.3.1"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4.42", default-features = false }
|
||||
chrono = { version = "0.4.41", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
futures = "0.3.31"
|
||||
futures-lite = "2.6.1"
|
||||
futures-lite = "2.6.0"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
mailparse = "0.16.1"
|
||||
nu-ansi-term = "0.50"
|
||||
nu-ansi-term = "0.46"
|
||||
num-traits = "0.2"
|
||||
rand = "0.9"
|
||||
rand = "0.8"
|
||||
regex = "1.10"
|
||||
rusqlite = "0.36"
|
||||
sanitize-filename = "0.5"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.23.0"
|
||||
tempfile = "3.20.0"
|
||||
thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.16"
|
||||
tokio-util = "0.7.14"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.6.4"
|
||||
|
||||
|
||||
27
README.md
27
README.md
@@ -80,41 +80,30 @@ Connect to your mail server (if already configured):
|
||||
> connect
|
||||
```
|
||||
|
||||
Export your public key to a vCard file:
|
||||
|
||||
```
|
||||
> make-vcard my.vcard 1
|
||||
```
|
||||
|
||||
Create contacts by address or vCard file:
|
||||
Create a contact:
|
||||
|
||||
```
|
||||
> addcontact yourfriends@email.org
|
||||
> import-vcard key-contact.vcard
|
||||
Command executed successfully.
|
||||
```
|
||||
|
||||
List contacts:
|
||||
|
||||
```
|
||||
> listcontacts
|
||||
Contact#Contact#11: key-contact@email.org <key-contact@email.org>
|
||||
Contact#Contact#Self: Me √ <your@email.org>
|
||||
2 key contacts.
|
||||
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
|
||||
1 address contacts.
|
||||
Contact#10: <name unset> <yourfriends@email.org>
|
||||
Contact#1: Me √√ <your@email.org>
|
||||
```
|
||||
|
||||
Create a chat with your friend and send a message:
|
||||
|
||||
```
|
||||
> createchat 10
|
||||
Single#Chat#12 created successfully.
|
||||
> chat 12
|
||||
Selecting chat Chat#12
|
||||
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.
|
||||
Single#10 created successfully.
|
||||
> chat 10
|
||||
Single#10: yourfriends@email.org [yourfriends@email.org]
|
||||
> send hi
|
||||
Message sent.
|
||||
```
|
||||
|
||||
List messages when inside a chat:
|
||||
|
||||
33
STYLE.md
33
STYLE.md
@@ -78,27 +78,6 @@ All errors should be handled in one of these ways:
|
||||
- With `.log_err().ok()`.
|
||||
- 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
|
||||
and `debug = 1` option is set in the test profile.
|
||||
This allows to run `RUST_BACKTRACE=1 cargo test`
|
||||
@@ -112,18 +91,6 @@ 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
|
||||
|
||||
For logging, use `info!`, `warn!` and `error!` macros.
|
||||
|
||||
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,11 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::hint::black_box;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use deltachat::Events;
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::Events;
|
||||
use tempfile::tempdir;
|
||||
|
||||
async fn address_book_benchmark(n: u32, read_count: u32) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
use std::hint::black_box;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use deltachat::accounts::Accounts;
|
||||
use tempfile::tempdir;
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
use std::hint::black_box;
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use deltachat::Events;
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::Events;
|
||||
|
||||
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
|
||||
let id = 100;
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
use std::hint::black_box;
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use deltachat::Events;
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::Events;
|
||||
|
||||
async fn get_chat_list_benchmark(context: &Context) {
|
||||
Chatlist::try_load(context, 0, None, None).await.unwrap();
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
use std::hint::black_box;
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
|
||||
use deltachat::Events;
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::Events;
|
||||
use futures_lite::future::block_on;
|
||||
use tempfile::tempdir;
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
use std::hint::black_box;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use deltachat::{
|
||||
Events,
|
||||
config::Config,
|
||||
context::Context,
|
||||
imex::{ImexMode, imex},
|
||||
imex::{imex, ImexMode},
|
||||
receive_imf::receive_imf,
|
||||
stock_str::StockStrings,
|
||||
Events,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
use std::hint::black_box;
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use deltachat::Events;
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::Events;
|
||||
|
||||
async fn search_benchmark(dbfile: impl AsRef<Path>) {
|
||||
let id = 100;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#![recursion_limit = "256"]
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
|
||||
@@ -68,7 +68,7 @@ impl ContactAddress {
|
||||
pub fn new(s: &str) -> Result<Self> {
|
||||
let addr = addr_normalize(s);
|
||||
if !may_be_valid_addr(&addr) {
|
||||
bail!("invalid address {s:?}");
|
||||
bail!("invalid address {:?}", s);
|
||||
}
|
||||
Ok(Self(addr.to_string()))
|
||||
}
|
||||
@@ -257,16 +257,16 @@ impl EmailAddress {
|
||||
.chars()
|
||||
.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[..] {
|
||||
[domain, local] => {
|
||||
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() {
|
||||
bail!("missing domain after '@' in {input:?}");
|
||||
bail!("missing domain after '@' in {:?}", input);
|
||||
}
|
||||
if domain.ends_with('.') {
|
||||
bail!("Domain {domain:?} should not contain the dot in the end");
|
||||
@@ -276,7 +276,7 @@ impl EmailAddress {
|
||||
domain: (*domain).to_string(),
|
||||
})
|
||||
}
|
||||
_ => bail!("Email {input:?} must contain '@' character"),
|
||||
_ => bail!("Email {:?} must contain '@' character", input),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.22.0"
|
||||
version = "1.160.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -415,6 +415,7 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* As for `displayname` and `selfstatus`, also the avatar is sent to the recipients.
|
||||
* To save traffic, however, the avatar is attached only as needed
|
||||
* and also recoded to a reasonable size.
|
||||
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
|
||||
* - `mdns_enabled` = 0=do not send or request read receipts,
|
||||
* 1=send and request read receipts
|
||||
* default=send and request read receipts, only send but not request if `bot` is set
|
||||
@@ -458,6 +459,12 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* The library uses the `media_quality` setting to use different defaults
|
||||
* for recoding images sent with type #DC_MSG_IMAGE.
|
||||
* If needed, recoding other file types is up to the UI.
|
||||
* - `webrtc_instance` = webrtc instance to use for videochats in the form
|
||||
* `[basicwebrtc:|jitsi:]https://example.com/subdir#roomname=$ROOM`
|
||||
* if the URL is prefixed by `basicwebrtc`, the server is assumed to be of the type
|
||||
* https://github.com/cracker0dks/basicwebrtc which some UIs have native support for.
|
||||
* The type `jitsi:` may be handled by external apps.
|
||||
* If no type is prefixed, the videochat is handled completely in a browser.
|
||||
* - `bot` = Set to "1" if this is a bot.
|
||||
* Prevents adding the "Device messages" and "Saved messages" chats,
|
||||
* adds Auto-Submitted header to outgoing messages,
|
||||
@@ -496,6 +503,13 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||
* seconds. 2 days by default.
|
||||
* This is not supposed to be changed by UIs and only used for testing.
|
||||
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
|
||||
* to 1 if it supports verified 1:1 chats.
|
||||
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
|
||||
* and when the key changes, an info message is posted into the chat.
|
||||
* 0=Nothing else happens when the key changes.
|
||||
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
|
||||
* until `dc_accept_chat()` is called.
|
||||
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
|
||||
* - `is_muted` = Whether a context is muted by the user.
|
||||
* Muted contexts should not sound, vibrate or show notifications.
|
||||
@@ -569,10 +583,11 @@ int dc_set_stock_translation(dc_context_t* context, uint32_t stock_i
|
||||
/**
|
||||
* Set configuration values from a QR code.
|
||||
* Before this function is called, dc_check_qr() should confirm the type of the
|
||||
* QR code is DC_QR_ACCOUNT or DC_QR_LOGIN.
|
||||
* QR code is DC_QR_ACCOUNT, DC_QR_LOGIN or DC_QR_WEBRTC_INSTANCE.
|
||||
*
|
||||
* Internally, the function will call dc_set_config() with the appropriate keys,
|
||||
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN.
|
||||
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN
|
||||
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
@@ -1045,6 +1060,42 @@ void dc_send_edit_request (dc_context_t* context, uint32_t ms
|
||||
void dc_send_delete_request (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
|
||||
|
||||
|
||||
/**
|
||||
* Send invitation to a videochat.
|
||||
*
|
||||
* This function reads the `webrtc_instance` config value,
|
||||
* may check that the server is working in some way
|
||||
* and creates a unique room for this chat, if needed doing a TOKEN roundtrip for that.
|
||||
*
|
||||
* After that, the function sends out a message that contains information to join the room:
|
||||
*
|
||||
* - To allow non-delta-clients to join the chat,
|
||||
* the message contains a text-area with some descriptive text
|
||||
* and a URL that can be opened in a supported browser to join the videochat.
|
||||
*
|
||||
* - delta-clients can get all information needed from
|
||||
* the message object, using e.g.
|
||||
* dc_msg_get_videochat_url() and check dc_msg_get_viewtype() for #DC_MSG_VIDEOCHAT_INVITATION.
|
||||
*
|
||||
* dc_send_videochat_invitation() is blocking and may take a while,
|
||||
* so the UIs will typically call the function from within a thread.
|
||||
* Moreover, UIs will typically enter the room directly without an additional click on the message,
|
||||
* for this purpose, the function returns the message id directly.
|
||||
*
|
||||
* As for other messages sent, this function
|
||||
* sends the event #DC_EVENT_MSGS_CHANGED on success, the message has a delivery state, and so on.
|
||||
* The recipient will get noticed by the call as usual by #DC_EVENT_INCOMING_MSG or #DC_EVENT_MSGS_CHANGED,
|
||||
* However, UIs might some things differently, e.g. play a different sound.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat to start a videochat for.
|
||||
* @return The ID of the message sent out
|
||||
* or 0 for errors.
|
||||
*/
|
||||
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* A webxdc instance sends a status update to its other members.
|
||||
*
|
||||
@@ -1171,117 +1222,6 @@ void dc_set_webxdc_integration (dc_context_t* context, const char* f
|
||||
uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Start an outgoing call.
|
||||
* This sends a message of type #DC_MSG_CALL with all relevant information to the callee,
|
||||
* who will get informed by an #DC_EVENT_INCOMING_CALL event and rings.
|
||||
*
|
||||
* Possible actions during ringing:
|
||||
*
|
||||
* - caller cancels the call using dc_end_call():
|
||||
* callee receives #DC_EVENT_CALL_ENDED and has a "Missed call"
|
||||
*
|
||||
* - callee accepts using dc_accept_incoming_call():
|
||||
* caller receives #DC_EVENT_OUTGOING_CALL_ACCEPTED.
|
||||
* callee's devices receive #DC_EVENT_INCOMING_CALL_ACCEPTED, call starts
|
||||
*
|
||||
* - callee declines using dc_end_call():
|
||||
* caller receives #DC_EVENT_CALL_ENDED and has a "Declinced Call".
|
||||
* callee's other devices receive #DC_EVENT_CALL_ENDED and have a "Canceled Call",
|
||||
*
|
||||
* - callee is already in a call:
|
||||
* what to do depends on the capabilities of UI to handle calls.
|
||||
* if UI cannot handle multiple calls, an easy approach would be to decline the new call automatically
|
||||
* and make that visble to the user in the call, e.g. by a notification
|
||||
*
|
||||
* - timeout:
|
||||
* after 1 minute without action,
|
||||
* caller and callee receive #DC_EVENT_CALL_ENDED
|
||||
* to prevent endless ringing of callee
|
||||
* in case caller got offline without being able to send cancellation message.
|
||||
* for caller, this is a "Canceled call";
|
||||
* for callee, this is a "Missed call"
|
||||
*
|
||||
* Actions during the call:
|
||||
*
|
||||
* - caller ends the call using dc_end_call():
|
||||
* callee receives #DC_EVENT_CALL_ENDED
|
||||
*
|
||||
* - callee ends the call using dc_end_call():
|
||||
* caller receives #DC_EVENT_CALL_ENDED
|
||||
*
|
||||
* Contact request handling:
|
||||
*
|
||||
* - placing or accepting calls implies accepting contact requests
|
||||
*
|
||||
* - ending a call does not accept a contact request;
|
||||
* instead, the call will timeout on all affected devices.
|
||||
*
|
||||
* Note, that the events are for updating the call screen,
|
||||
* possible status messages are added and updated as usual, including the known events.
|
||||
* In the UI, the sorted chatlist is used as an overview about calls as well as messages.
|
||||
* To place a call with a contact that has no chat yet, use dc_create_chat_by_contact_id() first.
|
||||
*
|
||||
* UI will usually allow only one call at the same time,
|
||||
* this has to be tracked by UI across profile, the core does not track this.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat to place a call for.
|
||||
* This needs to be a one-to-one chat.
|
||||
* @param place_call_info any data that other devices receive
|
||||
* in #DC_EVENT_INCOMING_CALL.
|
||||
* @return ID of the system message announcing the call.
|
||||
*/
|
||||
uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t chat_id, const char* place_call_info);
|
||||
|
||||
|
||||
/**
|
||||
* Accept incoming call.
|
||||
*
|
||||
* This implicitly accepts the contact request, if not yet done.
|
||||
* All affected devices will receive
|
||||
* either #DC_EVENT_OUTGOING_CALL_ACCEPTED or #DC_EVENT_INCOMING_CALL_ACCEPTED.
|
||||
*
|
||||
* If the call is already accepted or ended, nothing happens.
|
||||
* If the chat is a contact request, it is accepted implicitly.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The ID of the call to accept.
|
||||
* This is the ID reported by #DC_EVENT_INCOMING_CALL
|
||||
* and equals to the ID of the corresponding info message.
|
||||
* @param accept_call_info any data that other devices receive
|
||||
* in #DC_EVENT_OUTGOING_CALL_ACCEPTED.
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_accept_incoming_call (dc_context_t* context, uint32_t msg_id, const char* accept_call_info);
|
||||
|
||||
|
||||
/**
|
||||
* End incoming or outgoing call.
|
||||
*
|
||||
* For unaccepted calls ended by the caller, this is a "cancellation".
|
||||
* Unaccepted calls ended by the callee are a "decline".
|
||||
* If the call was accepted, this is a "hangup".
|
||||
*
|
||||
* All participant devices get informed about the ended call via #DC_EVENT_CALL_ENDED unless they are contact requests.
|
||||
* For contact requests, the call times out on all other affected devices.
|
||||
*
|
||||
* If the message ID is wrong or does not exist for whatever reasons, nothing happens.
|
||||
* Therefore, and for resilience, UI should remove the call UI directly when calling
|
||||
* this function and not only on the event.
|
||||
*
|
||||
* If the call is already ended, nothing happens.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id the ID of the call.
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_end_call (dc_context_t* context, uint32_t msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Save a draft for a chat in the database.
|
||||
*
|
||||
@@ -1399,14 +1339,12 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
|
||||
* Optionally, some special markers added to the ID array may help to
|
||||
* implement virtual lists.
|
||||
*
|
||||
* To get the concrete time of the message, use dc_array_get_timestamp().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The chat ID of which the messages IDs should be queried.
|
||||
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
|
||||
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
|
||||
* The day marker timestamp is the midnight one for the corresponding (following) day in the local timezone.
|
||||
* To get the concrete time of the marker, use dc_array_get_timestamp().
|
||||
* If set to DC_GCM_INFO_ONLY, only system messages will be returned, can be combined with DC_GCM_ADDDAYMARKER.
|
||||
* @param marker1before Deprecated, set this to 0.
|
||||
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
|
||||
@@ -1763,7 +1701,9 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param protect Deprecated 2025-08-31, ignored.
|
||||
* @param protect If set to 1 the function creates group with protection initially enabled.
|
||||
* Only verified members are allowed in these groups
|
||||
* and end-to-end-encryption is always enabled.
|
||||
* @param name The name of the group chat to create.
|
||||
* The name may be changed later using dc_set_chat_name().
|
||||
* To find out the name of a group later, see dc_chat_get_name()
|
||||
@@ -2154,19 +2094,9 @@ int dc_may_be_valid_addr (const char* addr);
|
||||
|
||||
|
||||
/**
|
||||
* 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 dc_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 dc_may_be_valid_addr().
|
||||
*
|
||||
@@ -2188,13 +2118,6 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
|
||||
* To add a number of contacts, see dc_add_address_book() which is much faster for adding
|
||||
* a bunch of addresses.
|
||||
*
|
||||
* This will always create or look up an address-contact,
|
||||
* i.e. a contact identified by an email address,
|
||||
* with all messages sent to and from this contact being unencrypted.
|
||||
* If the user just clicked on an email address,
|
||||
* you should first check `lookup_contact_id_by_addr`,
|
||||
* and only if there is no contact yet, call this function here.
|
||||
*
|
||||
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -2214,7 +2137,6 @@ uint32_t dc_create_contact (dc_context_t* context, const char*
|
||||
#define DC_GCL_DEPRECATED_VERIFIED_ONLY 0x01
|
||||
|
||||
#define DC_GCL_ADD_SELF 0x02
|
||||
#define DC_GCL_ADDRESS 0x04
|
||||
|
||||
|
||||
/**
|
||||
@@ -2270,13 +2192,11 @@ dc_array_t* dc_import_vcard (dc_context_t* context, const char*
|
||||
* Returns known and unblocked contacts.
|
||||
*
|
||||
* To get information about a single contact, see dc_get_contact().
|
||||
* By default, key-contacts are listed.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param flags A combination of flags:
|
||||
* - DC_GCL_ADD_SELF: SELF is added to the list unless filtered by other parameters
|
||||
* - DC_GCL_ADDRESS: List address-contacts instead of key-contacts.
|
||||
* - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters
|
||||
* @param query A string to filter the list. Typically used to implement an
|
||||
* incremental search. NULL for no filtering.
|
||||
* @return An array containing all contact IDs. Must be dc_array_unref()'d
|
||||
@@ -2570,6 +2490,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_BACKUP 251 // deprecated
|
||||
#define DC_QR_BACKUP2 252
|
||||
#define DC_QR_BACKUP_TOO_NEW 255
|
||||
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
|
||||
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
|
||||
#define DC_QR_ADDR 320 // id=contact
|
||||
#define DC_QR_TEXT 330 // text1=text
|
||||
@@ -2623,6 +2544,10 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* show a hint to the user that this backup comes from a newer Delta Chat version
|
||||
* and this device needs an update
|
||||
*
|
||||
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
|
||||
* ask the user if they want to use the given service for video chats;
|
||||
* if so, call dc_set_config_from_qr().
|
||||
*
|
||||
* - DC_QR_PROXY with dc_lot_t::text1=address:
|
||||
* ask the user if they want to use the given proxy.
|
||||
* if so, call dc_set_config_from_qr() and restart I/O.
|
||||
@@ -3888,29 +3813,49 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Deprecated, always returns 0.
|
||||
* Check if a chat is protected.
|
||||
*
|
||||
* End-to-end encryption is guaranteed in protected chats
|
||||
* and only verified contacts
|
||||
* as determined by dc_contact_is_verified()
|
||||
* can be added to protected chats.
|
||||
*
|
||||
* Protected chats are created using dc_create_group_chat()
|
||||
* by setting the 'protect' parameter to 1.
|
||||
* 1:1 chats become protected or unprotected automatically
|
||||
* if `verified_one_on_one_chats` setting is enabled.
|
||||
*
|
||||
* UI should display a green checkmark
|
||||
* in the chat title,
|
||||
* in the chatlist item
|
||||
* and in the chat profile
|
||||
* if chat protection is enabled.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return Always 0.
|
||||
* @deprecated 2025-09-09
|
||||
* @return 1=chat protected, 0=chat is not protected.
|
||||
*/
|
||||
int dc_chat_is_protected (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the chat is encrypted.
|
||||
* Checks if the chat was protected, and then an incoming message broke this protection.
|
||||
*
|
||||
* 1:1 chats with key-contacts and group chats with key-contacts
|
||||
* are encrypted.
|
||||
* 1:1 chats with emails contacts and ad-hoc groups
|
||||
* created for email threads are not encrypted.
|
||||
* This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
|
||||
* otherwise it will return false for all chats.
|
||||
*
|
||||
* 1:1 chats are automatically set as protected when a contact is verified.
|
||||
* When a message comes in that is not encrypted / signed correctly,
|
||||
* the chat is automatically set as unprotected again.
|
||||
* dc_chat_is_protection_broken() will return true until dc_accept_chat() is called.
|
||||
*
|
||||
* The UI should let the user confirm that this is OK with a message like
|
||||
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return 1=chat is encrypted, 0=chat is not encrypted.
|
||||
* @return 1=chat protection broken, 0=otherwise.
|
||||
*/
|
||||
int dc_chat_is_encrypted (const dc_chat_t *chat);
|
||||
int dc_chat_is_protection_broken (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
@@ -4340,16 +4285,11 @@ int dc_msg_get_duration (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Check if message was correctly encrypted and signed.
|
||||
*
|
||||
* Historically, UIs showed a small padlock on the message then.
|
||||
* Today, the UIs should instead
|
||||
* show a small email-icon on the message if the message is not encrypted or signed,
|
||||
* and nothing otherwise.
|
||||
* Check if a padlock should be shown beside the message.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return 1=message correctly encrypted and signed, no need to show anything; 0=show email-icon beside the message.
|
||||
* @return 1=padlock should be shown beside message, 0=do not show a padlock beside the message.
|
||||
*/
|
||||
int dc_msg_get_showpadlock (const dc_msg_t* msg);
|
||||
|
||||
@@ -4572,12 +4512,12 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
||||
* - DC_INFO_MEMBER_ADDED_TO_GROUP (4) - "Member CONTACT added by OTHER_CONTACT"
|
||||
* - DC_INFO_MEMBER_REMOVED_FROM_GROUP (5) - "Member CONTACT removed by OTHER_CONTACT"
|
||||
* - DC_INFO_EPHEMERAL_TIMER_CHANGED (10) - "Disappearing messages CHANGED_TO by CONTACT"
|
||||
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is protected"
|
||||
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
|
||||
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
|
||||
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
|
||||
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
|
||||
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
|
||||
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
|
||||
* - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted"
|
||||
*
|
||||
* For the messages that refer to a CONTACT,
|
||||
* dc_msg_get_info_contact_id() returns the contact ID.
|
||||
@@ -4630,10 +4570,9 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
|
||||
#define DC_INFO_LOCATION_ONLY 9
|
||||
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
|
||||
#define DC_INFO_PROTECTION_ENABLED 11
|
||||
#define DC_INFO_PROTECTION_DISABLED 12 // deprecated 2025-07
|
||||
#define DC_INFO_PROTECTION_DISABLED 12
|
||||
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
|
||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||
#define DC_INFO_CHAT_E2EE 50
|
||||
|
||||
|
||||
/**
|
||||
@@ -4688,6 +4627,22 @@ int dc_msg_is_setupmessage (const dc_msg_t* msg);
|
||||
char* dc_msg_get_setupcodebegin (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get URL of a videochat invitation.
|
||||
*
|
||||
* Videochat invitations are sent out using dc_send_videochat_invitation()
|
||||
* and dc_msg_get_viewtype() returns #DC_MSG_VIDEOCHAT_INVITATION for such invitations.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return If the message contains a videochat invitation,
|
||||
* the URL of the invitation is returned.
|
||||
* If the message is no videochat invitation, NULL is returned.
|
||||
* Must be released using dc_str_unref() when done.
|
||||
*/
|
||||
char* dc_msg_get_videochat_url (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Gets the error status of the message.
|
||||
* If there is no error associated with the message, NULL is returned.
|
||||
@@ -4710,6 +4665,41 @@ char* dc_msg_get_setupcodebegin (const dc_msg_t* msg);
|
||||
char* dc_msg_get_error (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get type of videochat.
|
||||
*
|
||||
* Calling this functions only makes sense for messages of type #DC_MSG_VIDEOCHAT_INVITATION,
|
||||
* in this case, if `basicwebrtc:` as of https://github.com/cracker0dks/basicwebrtc or `jitsi`
|
||||
* were used to initiate the videochat,
|
||||
* dc_msg_get_videochat_type() returns the corresponding type.
|
||||
*
|
||||
* The videochat URL can be retrieved using dc_msg_get_videochat_url().
|
||||
* To check if a message is a videochat invitation at all, check the message type for #DC_MSG_VIDEOCHAT_INVITATION.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return Type of the videochat as of DC_VIDEOCHATTYPE_BASICWEBRTC, DC_VIDEOCHATTYPE_JITSI or DC_VIDEOCHATTYPE_UNKNOWN.
|
||||
*
|
||||
* Example:
|
||||
* ~~~
|
||||
* if (dc_msg_get_viewtype(msg) == DC_MSG_VIDEOCHAT_INVITATION) {
|
||||
* if (dc_msg_get_videochat_type(msg) == DC_VIDEOCHATTYPE_BASICWEBRTC) {
|
||||
* // videochat invitation that we ship a client for
|
||||
* } else {
|
||||
* // use browser for videochat - or add an additional check for DC_VIDEOCHATTYPE_JITSI
|
||||
* }
|
||||
* } else {
|
||||
* // not a videochat invitation
|
||||
* }
|
||||
* ~~~
|
||||
*/
|
||||
int dc_msg_get_videochat_type (const dc_msg_t* msg);
|
||||
|
||||
#define DC_VIDEOCHATTYPE_UNKNOWN 0
|
||||
#define DC_VIDEOCHATTYPE_BASICWEBRTC 1
|
||||
#define DC_VIDEOCHATTYPE_JITSI 2
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the message has a full HTML version.
|
||||
*
|
||||
@@ -5253,14 +5243,20 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Check if the contact
|
||||
* can be added to protected chats.
|
||||
* can be added to verified chats,
|
||||
* i.e. has a verified key
|
||||
* and Autocrypt key matches the verified key.
|
||||
*
|
||||
* See dc_contact_get_verifier_id() for a guidance how to display these information.
|
||||
* If contact is verified
|
||||
* 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 (otherwise, use dc_chat_is_protected()).
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return 0: contact is not verified.
|
||||
* 2: SELF and contact have verified their fingerprints in both directions.
|
||||
* 2: SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
|
||||
*/
|
||||
int dc_contact_is_verified (dc_contact_t* contact);
|
||||
|
||||
@@ -5274,39 +5270,19 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
||||
int dc_contact_is_bot (dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether contact is a key-contact,
|
||||
* i.e. it is identified by the public key
|
||||
* rather than the email address.
|
||||
*
|
||||
* If so, all messages to and from this contact are encrypted.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return 1 if the contact is a key-contact, 0 if it is an address-contact.
|
||||
*/
|
||||
int dc_contact_is_key_contact (dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Return the contact ID that verified a contact.
|
||||
*
|
||||
* As verifier may be unknown,
|
||||
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
|
||||
* If the function returns non-zero result,
|
||||
* display green checkmark in the profile and "Introduced by ..." line
|
||||
* with the name and address of the contact
|
||||
* formatted by dc_contact_get_name_n_addr.
|
||||
*
|
||||
* UI should display the information in the contact's profile as follows:
|
||||
*
|
||||
* - If dc_contact_get_verifier_id() != 0,
|
||||
* display text "Introduced by ..."
|
||||
* with the name and address of the contact
|
||||
* formatted by dc_contact_get_name_n_addr().
|
||||
* Prefix the text by a green checkmark.
|
||||
*
|
||||
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
|
||||
* display "Introduced" prefixed by a green checkmark.
|
||||
*
|
||||
* - if dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() == 0,
|
||||
* display nothing
|
||||
* If this function returns a verifier,
|
||||
* this does not necessarily mean
|
||||
* you can add the contact to verified chats.
|
||||
* Use dc_contact_is_verified() to check
|
||||
* if a contact can be added to a verified chat instead.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -5342,9 +5318,11 @@ dc_provider_t* dc_provider_new_from_email (const dc_context_t* conte
|
||||
|
||||
|
||||
/**
|
||||
* Create a provider struct for the given e-mail address by local lookup.
|
||||
* Create a provider struct for the given e-mail address by local and DNS lookup.
|
||||
*
|
||||
* DNS lookup is not used anymore and this function is deprecated.
|
||||
* First lookup is done from the local database as of dc_provider_new_from_email().
|
||||
* If the first lookup fails, an additional DNS lookup is done,
|
||||
* trying to figure out the provider belonging to custom domains.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param context The context object.
|
||||
@@ -5352,7 +5330,6 @@ dc_provider_t* dc_provider_new_from_email (const dc_context_t* conte
|
||||
* @return A dc_provider_t struct which can be used with the dc_provider_get_*
|
||||
* accessor functions. If no provider info is found, NULL will be
|
||||
* returned.
|
||||
* @deprecated 2025-10-17 use dc_provider_new_from_email() instead.
|
||||
*/
|
||||
dc_provider_t* dc_provider_new_from_email_with_dns (const dc_context_t* context, const char* email);
|
||||
|
||||
@@ -5608,21 +5585,14 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
|
||||
|
||||
/**
|
||||
* Message indicating an incoming or outgoing call.
|
||||
* Message indicating an incoming or outgoing videochat.
|
||||
* The message was created via dc_send_videochat_invitation() on this or a remote device.
|
||||
*
|
||||
* These messages are created by dc_place_outgoing_call()
|
||||
* and should be rendered by UI similar to text messages,
|
||||
* maybe with some "phone icon" at the side.
|
||||
*
|
||||
* The message text is updated as needed
|
||||
* and UI will be informed via #DC_EVENT_MSGS_CHANGED as usual.
|
||||
*
|
||||
* Do not start ringing when seeing this message;
|
||||
* the mesage may belong e.g. to an old missed call.
|
||||
*
|
||||
* Instead, ringing should start on the event #DC_EVENT_INCOMING_CALL
|
||||
* Typically, such messages are rendered differently by the UIs,
|
||||
* e.g. contain a button to join the videochat.
|
||||
* The URL for joining can be retrieved using dc_msg_get_videochat_url().
|
||||
*/
|
||||
#define DC_MSG_CALL 71
|
||||
#define DC_MSG_VIDEOCHAT_INVITATION 70
|
||||
|
||||
|
||||
/**
|
||||
@@ -5745,33 +5715,9 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
#define DC_CHAT_TYPE_MAILINGLIST 140
|
||||
|
||||
/**
|
||||
* Outgoing broadcast channel, called "Channel" in the UI.
|
||||
*
|
||||
* The user can send into this chat,
|
||||
* and all recipients will receive messages
|
||||
* in a `DC_CHAT_TYPE_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.
|
||||
* A broadcast list. See dc_chat_get_type() for details.
|
||||
*/
|
||||
#define DC_CHAT_TYPE_OUT_BROADCAST 160
|
||||
|
||||
/**
|
||||
* Incoming broadcast channel, called "Channel" in the UI.
|
||||
*
|
||||
* This chat is read-only,
|
||||
* and we do not know who the other recipients are.
|
||||
*
|
||||
* This is similar to `DC_CHAT_TYPE_MAILINGLIST`,
|
||||
* with the main difference being that
|
||||
* broadcasts 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.
|
||||
*/
|
||||
#define DC_CHAT_TYPE_IN_BROADCAST 165
|
||||
#define DC_CHAT_TYPE_BROADCAST 160
|
||||
|
||||
/**
|
||||
* @}
|
||||
@@ -6378,6 +6324,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/**
|
||||
* 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 dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
||||
* and dc_remove_contact_from_chat().
|
||||
*
|
||||
@@ -6467,7 +6414,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
* generated by dc_get_securejoin_qr().
|
||||
*
|
||||
* @param data1 (int) The ID of the contact that wants to join.
|
||||
* @param data2 (int) The progress, always 1000.
|
||||
* @param data2 (int) The progress as:
|
||||
* 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||
* 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
|
||||
* 1000=Protocol finished for this contact.
|
||||
*/
|
||||
#define DC_EVENT_SECUREJOIN_INVITER_PROGRESS 2060
|
||||
|
||||
@@ -6619,60 +6570,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_CHANNEL_OVERFLOW 2400
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Incoming call.
|
||||
* UI will usually start ringing,
|
||||
* or show a notification if there is already a call in some profile.
|
||||
*
|
||||
* Together with this event,
|
||||
* a message of type #DC_MSG_CALL is added to the corresponding chat;
|
||||
* this message is announced and updated by the usual event as #DC_EVENT_MSGS_CHANGED,
|
||||
* there is usually no need to take care of this message from any of the CALL events.
|
||||
*
|
||||
* If user takes action, dc_accept_incoming_call() or dc_end_call() should be called.
|
||||
*
|
||||
* Otherwise, ringing should end on #DC_EVENT_CALL_ENDED
|
||||
* or #DC_EVENT_INCOMING_CALL_ACCEPTED
|
||||
*
|
||||
* @param data1 (int) msg_id ID of the message referring to the call.
|
||||
* @param data2 (char*) place_call_info, text passed to dc_place_outgoing_call()
|
||||
* @param data2 (int) 1 if incoming call is a video call, 0 otherwise
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_CALL 2550
|
||||
|
||||
/**
|
||||
* The callee accepted an incoming call on this or another device using dc_accept_incoming_call().
|
||||
* The caller gets the event #DC_EVENT_OUTGOING_CALL_ACCEPTED at the same time.
|
||||
*
|
||||
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
|
||||
*
|
||||
* @param data1 (int) msg_id ID of the message referring to the call
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_CALL_ACCEPTED 2560
|
||||
|
||||
/**
|
||||
* A call placed using dc_place_outgoing_call() was accepted by the callee using dc_accept_incoming_call().
|
||||
*
|
||||
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
|
||||
*
|
||||
* @param data1 (int) msg_id ID of the message referring to the call
|
||||
* @param data2 (char*) accept_call_info, text passed to dc_accept_incoming_call()
|
||||
*/
|
||||
#define DC_EVENT_OUTGOING_CALL_ACCEPTED 2570
|
||||
|
||||
/**
|
||||
* An incoming or outgoing call was ended using dc_end_call() on this or another device, by caller or callee.
|
||||
* Moreover, the event is sent when the call was not accepted within 1 minute timeout.
|
||||
*
|
||||
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
|
||||
*
|
||||
* @param data1 (int) msg_id ID of the message referring to the call
|
||||
*/
|
||||
#define DC_EVENT_CALL_ENDED 2580
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@@ -6940,7 +6837,9 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in summaries.
|
||||
#define DC_STR_GIF 23
|
||||
|
||||
/// @deprecated 2025-07, this string is no longer needed.
|
||||
/// "Encrypted message"
|
||||
///
|
||||
/// Used in subjects of outgoing messages.
|
||||
#define DC_STR_ENCRYPTEDMSG 24
|
||||
|
||||
/// "End-to-end encryption available."
|
||||
@@ -6956,6 +6855,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used to build the string returned by dc_get_contact_encrinfo().
|
||||
#define DC_STR_ENCR_NONE 28
|
||||
|
||||
/// "This message was encrypted for another setup."
|
||||
///
|
||||
/// Used as message text if decryption fails.
|
||||
#define DC_STR_CANTDECRYPT_MSG_BODY 29
|
||||
|
||||
/// "Fingerprints"
|
||||
///
|
||||
/// Used to build the string returned by dc_get_contact_encrinfo().
|
||||
@@ -6982,7 +6886,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "End-to-end encryption preferred."
|
||||
///
|
||||
/// Used to build the string returned by dc_get_contact_encrinfo().
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_E2E_PREFERRED 34
|
||||
|
||||
/// "%1$s verified"
|
||||
@@ -6995,14 +6898,12 @@ void dc_event_unref(dc_event_t* event);
|
||||
///
|
||||
/// Used in status messages.
|
||||
/// - %1$s will be replaced by the name of the contact that cannot be verified
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_CONTACT_NOT_VERIFIED 36
|
||||
|
||||
/// "Changed setup for %1$s."
|
||||
///
|
||||
/// Used in status messages.
|
||||
/// - %1$s will be replaced by the name of the contact with the changed setup
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_CONTACT_SETUP_CHANGED 37
|
||||
|
||||
/// "Archived chats"
|
||||
@@ -7088,8 +6989,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "Unknown sender for this chat. See 'info' for more details."
|
||||
///
|
||||
/// Use as message text if assigning the message to a chat is not totally correct.
|
||||
///
|
||||
/// @deprecated 2025-08-18
|
||||
#define DC_STR_UNKNOWN_SENDER_FOR_CHAT 72
|
||||
|
||||
/// "Message from %1$s"
|
||||
@@ -7152,6 +7051,17 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// @deprecated Deprecated 2021-01-30, DC_STR_EPHEMERAL_WEEKS is used instead.
|
||||
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
|
||||
|
||||
/// "Video chat invitation"
|
||||
///
|
||||
/// Used in summaries.
|
||||
#define DC_STR_VIDEOCHAT_INVITATION 82
|
||||
|
||||
/// "You are invited to a video chat, click %1$s to join."
|
||||
///
|
||||
/// Used as message text of outgoing video chat invitations.
|
||||
/// - %1$s will be replaced by the URL of the video chat
|
||||
#define DC_STR_VIDEOCHAT_INVITE_MSG_BODY 83
|
||||
|
||||
/// "Error: %1$s"
|
||||
///
|
||||
/// Used in error strings.
|
||||
@@ -7383,7 +7293,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "%1$s changed their address from %2$s to %3$s"
|
||||
///
|
||||
/// Used as an info message to chats with contacts that changed their address.
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_AEAP_ADDR_CHANGED 122
|
||||
|
||||
/// "You changed your email address from %1$s to %2$s.
|
||||
@@ -7621,18 +7530,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%2$s` will be replaced by name and address of the contact.
|
||||
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
|
||||
|
||||
/// "You set message deletion timer to 1 year."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_YEAR_BY_YOU 158
|
||||
|
||||
/// "Message deletion timer is set to 1 year by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_YEAR_BY_OTHER 159
|
||||
|
||||
/// "Scan to set up second device for %1$s"
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the account.
|
||||
@@ -7643,7 +7540,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as a device message after a successful backup transfer.
|
||||
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
|
||||
|
||||
/// "Messages are end-to-end encrypted."
|
||||
/// "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
///
|
||||
/// Used in info messages.
|
||||
#define DC_STR_CHAT_PROTECTION_ENABLED 170
|
||||
@@ -7651,7 +7548,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "%1$s sent a message from another device."
|
||||
///
|
||||
/// Used in info messages.
|
||||
/// @deprecated 2025-07
|
||||
#define DC_STR_CHAT_PROTECTION_DISABLED 171
|
||||
|
||||
/// "Others will only see this group after you sent a first message."
|
||||
@@ -7703,38 +7599,10 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "The contact must be online to proceed. This process will continue automatically in background."
|
||||
///
|
||||
/// Used as info message.
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
|
||||
|
||||
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
|
||||
#define DC_STR_DONATION_REQUEST 193
|
||||
|
||||
/// "Outgoing call"
|
||||
#define DC_STR_OUTGOING_CALL 194
|
||||
|
||||
/// "Incoming call"
|
||||
#define DC_STR_INCOMING_CALL 195
|
||||
|
||||
/// "Declined call"
|
||||
#define DC_STR_DECLINED_CALL 196
|
||||
|
||||
/// "Canceled call"
|
||||
#define DC_STR_CANCELED_CALL 197
|
||||
|
||||
/// "Missed call"
|
||||
#define DC_STR_MISSED_CALL 198
|
||||
|
||||
/// "You left the channel."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_CHANNEL_LEFT_BY_YOU 200
|
||||
|
||||
/// "Scan to join channel %1$s"
|
||||
///
|
||||
/// Subtitle for channel join qrcode svg image generated by the core.
|
||||
///
|
||||
/// `%1$s` will be replaced with the channel name.
|
||||
#define DC_STR_SECURE_JOIN_CHANNEL_QR_DESC 201
|
||||
/// "Contact". Deprecated, currently unused.
|
||||
#define DC_STR_CONTACT 200
|
||||
|
||||
/**
|
||||
* @}
|
||||
|
||||
@@ -22,7 +22,7 @@ use std::sync::{Arc, LazyLock};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration};
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, ContactId, Origin};
|
||||
use deltachat::context::{Context, ContextBuilder};
|
||||
@@ -39,6 +39,7 @@ use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
use message::Viewtype;
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
use rand::Rng;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::task::JoinHandle;
|
||||
@@ -100,7 +101,7 @@ pub unsafe extern "C" fn dc_context_new(
|
||||
|
||||
let ctx = if blobdir.is_null() || *blobdir == 0 {
|
||||
// generate random ID as this functionality is not yet available on the C-api.
|
||||
let id = rand::random();
|
||||
let id = rand::thread_rng().gen();
|
||||
block_on(
|
||||
ContextBuilder::new(as_path(dbfile).to_path_buf())
|
||||
.with_id(id)
|
||||
@@ -128,7 +129,7 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let id = rand::random();
|
||||
let id = rand::thread_rng().gen();
|
||||
match block_on(
|
||||
ContextBuilder::new(as_path(dbfile).to_path_buf())
|
||||
.with_id(id)
|
||||
@@ -374,7 +375,7 @@ pub unsafe extern "C" fn dc_get_connectivity(context: *const dc_context_t) -> li
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
ctx.get_connectivity() as u32 as libc::c_int
|
||||
block_on(ctx.get_connectivity()) as u32 as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -555,10 +556,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::AccountsChanged => 2302,
|
||||
EventType::AccountsItemChanged => 2303,
|
||||
EventType::EventChannelOverflow { .. } => 2400,
|
||||
EventType::IncomingCall { .. } => 2550,
|
||||
EventType::IncomingCallAccepted { .. } => 2560,
|
||||
EventType::OutgoingCallAccepted { .. } => 2570,
|
||||
EventType::CallEnded { .. } => 2580,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
@@ -622,11 +619,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
EventType::WebxdcRealtimeData { msg_id, .. }
|
||||
| EventType::WebxdcStatusUpdate { msg_id, .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { msg_id }
|
||||
| EventType::WebxdcInstanceDeleted { msg_id, .. }
|
||||
| EventType::IncomingCall { msg_id, .. }
|
||||
| EventType::IncomingCallAccepted { msg_id, .. }
|
||||
| EventType::OutgoingCallAccepted { msg_id, .. }
|
||||
| EventType::CallEnded { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
| EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::ChatlistItemChanged { chat_id } => {
|
||||
chat_id.unwrap_or_default().to_u32() as libc::c_int
|
||||
}
|
||||
@@ -678,9 +671,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::ChatDeleted { .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::IncomingCallAccepted { .. }
|
||||
| EventType::OutgoingCallAccepted { .. }
|
||||
| EventType::CallEnded { .. }
|
||||
| EventType::EventChannelOverflow { .. } => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
@@ -699,8 +689,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
..
|
||||
} => status_update_serial.to_u32() as libc::c_int,
|
||||
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
|
||||
EventType::IncomingCall { has_video, .. } => *has_video as libc::c_int,
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
@@ -779,21 +767,8 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::AccountsChanged
|
||||
| EventType::AccountsItemChanged
|
||||
| EventType::IncomingCallAccepted { .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. } => ptr::null_mut(),
|
||||
EventType::IncomingCall {
|
||||
place_call_info, ..
|
||||
} => {
|
||||
let data2 = place_call_info.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::OutgoingCallAccepted {
|
||||
accept_call_info, ..
|
||||
} => {
|
||||
let data2 = accept_call_info.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::CallEnded { .. } | EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
comment.to_c_string().unwrap_or_default().into_raw()
|
||||
@@ -1097,6 +1072,25 @@ pub unsafe extern "C" fn dc_send_delete_request(
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_videochat_invitation(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_send_videochat_invitation()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
chat::send_videochat_invitation(ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.unwrap_or_log_default(ctx, "Failed to send video chat invitation")
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1173,61 +1167,6 @@ pub unsafe extern "C" fn dc_init_webxdc_integration(
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_place_outgoing_call(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
place_call_info: *const libc::c_char,
|
||||
) -> u32 {
|
||||
if context.is_null() || chat_id == 0 {
|
||||
eprintln!("ignoring careless call to dc_place_outgoing_call()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
let place_call_info = to_string_lossy(place_call_info);
|
||||
|
||||
block_on(ctx.place_outgoing_call(chat_id, place_call_info))
|
||||
.context("Failed to place call")
|
||||
.log_err(ctx)
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.unwrap_or_log_default(ctx, "Failed to place call")
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accept_incoming_call(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
accept_call_info: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || msg_id == 0 {
|
||||
eprintln!("ignoring careless call to dc_accept_incoming_call()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let msg_id = MsgId::new(msg_id);
|
||||
let accept_call_info = to_string_lossy(accept_call_info);
|
||||
|
||||
block_on(ctx.accept_incoming_call(msg_id, accept_call_info))
|
||||
.context("Failed to accept call")
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_end_call(context: *mut dc_context_t, msg_id: u32) -> libc::c_int {
|
||||
if context.is_null() || msg_id == 0 {
|
||||
eprintln!("ignoring careless call to dc_end_call()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let msg_id = MsgId::new(msg_id);
|
||||
|
||||
block_on(ctx.end_call(msg_id))
|
||||
.context("Failed to end call")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_draft(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1720,7 +1659,7 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) -
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_create_group_chat(
|
||||
context: *mut dc_context_t,
|
||||
_protect: libc::c_int,
|
||||
protect: libc::c_int,
|
||||
name: *const libc::c_char,
|
||||
) -> u32 {
|
||||
if context.is_null() || name.is_null() {
|
||||
@@ -1728,12 +1667,22 @@ pub unsafe extern "C" fn dc_create_group_chat(
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(chat::create_group(ctx, &to_string_lossy(name)))
|
||||
.context("Failed to create group chat")
|
||||
let Some(protect) = ProtectionStatus::from_i32(protect)
|
||||
.context("Bad protect-value for dc_create_group_chat()")
|
||||
.log_err(ctx)
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
.ok()
|
||||
else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
chat::create_group_chat(ctx, protect, &to_string_lossy(name))
|
||||
.await
|
||||
.context("Failed to create group chat")
|
||||
.log_err(ctx)
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1743,8 +1692,8 @@ pub unsafe extern "C" fn dc_create_broadcast_list(context: *mut dc_context_t) ->
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(chat::create_broadcast(ctx, "Channel".to_string()))
|
||||
.context("Failed to create broadcast channel")
|
||||
block_on(chat::create_broadcast_list(ctx))
|
||||
.context("Failed to create broadcast list")
|
||||
.log_err(ctx)
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
@@ -3195,20 +3144,23 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dc_chat_is_protected(_chat: *mut dc_chat_t) -> libc::c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_is_encrypted(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_is_encrypted()");
|
||||
eprintln!("ignoring careless call to dc_chat_is_protected()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat.chat.is_protected() as libc::c_int
|
||||
}
|
||||
|
||||
block_on(ffi_chat.chat.is_encrypted(&ffi_chat.context))
|
||||
.unwrap_or_log_default(&ffi_chat.context, "Failed dc_chat_is_encrypted") as libc::c_int
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_is_protection_broken()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat.chat.is_protection_broken() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3819,6 +3771,31 @@ pub unsafe extern "C" fn dc_msg_has_html(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
ffi_msg.message.has_html().into()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_videochat_url(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_videochat_url()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
|
||||
ffi_msg
|
||||
.message
|
||||
.get_videochat_url()
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_videochat_type(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_videochat_type()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_videochat_type().unwrap_or_default() as i32
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_setupcodebegin(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
@@ -4314,15 +4291,6 @@ pub unsafe extern "C" fn dc_contact_is_bot(contact: *mut dc_contact_t) -> libc::
|
||||
(*contact).contact.is_bot() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_is_key_contact(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_is_key_contact()");
|
||||
return 0;
|
||||
}
|
||||
(*contact).contact.is_key_contact() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> u32 {
|
||||
if contact.is_null() {
|
||||
@@ -4335,7 +4303,6 @@ pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t)
|
||||
.context("failed to get verifier")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
|
||||
verifier_contact_id.to_u32()
|
||||
@@ -4645,9 +4612,13 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
|
||||
|
||||
let ctx = &*context;
|
||||
|
||||
match provider::get_provider_info_by_addr(addr.as_str())
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
match block_on(provider::get_provider_info_by_addr(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
true,
|
||||
))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
@@ -4666,13 +4637,25 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
|
||||
let addr = to_string_lossy(addr);
|
||||
|
||||
let ctx = &*context;
|
||||
let proxy_enabled = block_on(ctx.get_config_bool(config::Config::ProxyEnabled))
|
||||
.context("Can't get config")
|
||||
.log_err(ctx);
|
||||
|
||||
match provider::get_provider_info_by_addr(addr.as_str())
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
match proxy_enabled {
|
||||
Ok(proxy_enabled) => {
|
||||
match block_on(provider::get_provider_info_by_addr(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
proxy_enabled,
|
||||
))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ impl Lot {
|
||||
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Backup2 { .. } => None,
|
||||
Qr::BackupTooNew { .. } => None,
|
||||
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
|
||||
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
|
||||
Qr::Url { url } => Some(Cow::Borrowed(url)),
|
||||
@@ -104,6 +105,7 @@ impl Lot {
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Proxy { .. } => LotState::QrProxy,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
Qr::Url { .. } => LotState::QrUrl,
|
||||
@@ -130,6 +132,7 @@ impl Lot {
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::Backup2 { .. } => Default::default(),
|
||||
Qr::BackupTooNew { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Proxy { .. } => Default::default(),
|
||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::Url { .. } => Default::default(),
|
||||
@@ -182,6 +185,9 @@ pub enum LotState {
|
||||
|
||||
QrBackupTooNew = 255,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
/// text1=address, text2=protocol
|
||||
QrProxy = 271,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.22.0"
|
||||
version = "1.160.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -8,10 +8,10 @@ use std::{collections::HashMap, str::FromStr};
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
pub use deltachat::accounts::Accounts;
|
||||
use deltachat::blob::BlobObject;
|
||||
use deltachat::calls::ice_servers;
|
||||
use deltachat::chat::{
|
||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
||||
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||
ProtectionStatus,
|
||||
};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::config::Config;
|
||||
@@ -47,26 +47,25 @@ pub mod types;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
use types::account::Account;
|
||||
use types::calls::JsonrpcCallInfo;
|
||||
use types::chat::FullChat;
|
||||
use types::contact::{ContactObject, VcardContact};
|
||||
use types::events::Event;
|
||||
use types::http::HttpResponse;
|
||||
use types::message::{MessageData, MessageObject, MessageReadReceipt};
|
||||
use types::provider_info::ProviderInfo;
|
||||
use types::reactions::JsonrpcReactions;
|
||||
use types::reactions::JSONRPCReactions;
|
||||
use types::webxdc::WebxdcMessageInfo;
|
||||
|
||||
use self::types::message::{MessageInfo, MessageLoadResult};
|
||||
use self::types::{
|
||||
chat::{BasicChat, JsonrpcChatVisibility, MuteDuration},
|
||||
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
|
||||
location::JsonrpcLocation,
|
||||
message::{
|
||||
JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
|
||||
JSONRPCMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
|
||||
},
|
||||
};
|
||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
|
||||
use crate::api::types::qr::QrObject;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AccountState {
|
||||
@@ -92,8 +91,7 @@ pub struct CommandApi {
|
||||
|
||||
/// Receiver side of the event channel.
|
||||
///
|
||||
/// Events from it can be received by calling
|
||||
/// [`CommandApi::get_next_event`] method.
|
||||
/// Events from it can be received by calling `get_next_event` method.
|
||||
event_emitter: Arc<EventEmitter>,
|
||||
|
||||
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
|
||||
@@ -125,7 +123,7 @@ impl CommandApi {
|
||||
.read()
|
||||
.await
|
||||
.get_account(id)
|
||||
.ok_or_else(|| anyhow!("account with id {id} not found"))?;
|
||||
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
|
||||
Ok(sc)
|
||||
}
|
||||
|
||||
@@ -175,15 +173,7 @@ impl CommandApi {
|
||||
get_info()
|
||||
}
|
||||
|
||||
/// Get the next event, and remove it from the event queue.
|
||||
///
|
||||
/// If no events have happened since the last `get_next_event`
|
||||
/// (i.e. if the event queue is empty), the response will be returned
|
||||
/// only when a new event fires.
|
||||
///
|
||||
/// Note that if you are using the `BaseDeltaChat` JavaScript class
|
||||
/// or the `Rpc` Python class, this function will be invoked
|
||||
/// by those classes internally and should not be used manually.
|
||||
/// Get the next event.
|
||||
async fn get_next_event(&self) -> Result<Event> {
|
||||
self.event_emitter
|
||||
.recv()
|
||||
@@ -234,14 +224,6 @@ impl CommandApi {
|
||||
self.accounts.read().await.get_selected_account_id()
|
||||
}
|
||||
|
||||
/// Set the order of accounts.
|
||||
/// The provided list should contain all account IDs in the desired order.
|
||||
/// If an account ID is missing from the list, it will be appended at the end.
|
||||
/// If the list contains non-existent account IDs, they will be ignored.
|
||||
async fn set_accounts_order(&self, order: Vec<u32>) -> Result<()> {
|
||||
self.accounts.write().await.set_accounts_order(order).await
|
||||
}
|
||||
|
||||
/// Get a list of all configured accounts.
|
||||
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
|
||||
let mut accounts = Vec::new();
|
||||
@@ -307,7 +289,8 @@ impl CommandApi {
|
||||
Ok(Account::from_context(&ctx, account_id).await?)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"account with id {account_id} doesn't exist anymore"
|
||||
"account with id {} doesn't exist anymore",
|
||||
account_id
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -335,10 +318,21 @@ impl CommandApi {
|
||||
/// instead of the domain.
|
||||
async fn get_provider_info(
|
||||
&self,
|
||||
_account_id: u32,
|
||||
account_id: u32,
|
||||
email: String,
|
||||
) -> Result<Option<ProviderInfo>> {
|
||||
let provider_info = get_provider_info(email.split('@').next_back().unwrap_or(""));
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
let proxy_enabled = ctx
|
||||
.get_config_bool(deltachat::config::Config::ProxyEnabled)
|
||||
.await?;
|
||||
|
||||
let provider_info = get_provider_info(
|
||||
&ctx,
|
||||
email.split('@').next_back().unwrap_or(""),
|
||||
proxy_enabled,
|
||||
)
|
||||
.await;
|
||||
Ok(ProviderInfo::from_dc_type(provider_info))
|
||||
}
|
||||
|
||||
@@ -360,20 +354,6 @@ impl CommandApi {
|
||||
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
|
||||
}
|
||||
|
||||
/// If there was an error while the account was opened
|
||||
/// and migrated to the current version,
|
||||
/// then this function returns it.
|
||||
///
|
||||
/// This function is useful because the key-contacts migration could fail due to bugs
|
||||
/// and then the account will not work properly.
|
||||
///
|
||||
/// After opening an account, the UI should call this function
|
||||
/// and show the error string if one is returned.
|
||||
async fn get_migration_error(&self, account_id: u32) -> Result<Option<String>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx.get_migration_error())
|
||||
}
|
||||
|
||||
/// Copy file to blob dir.
|
||||
async fn copy_to_blob_dir(&self, account_id: u32, path: String) -> Result<PathBuf> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -381,6 +361,11 @@ impl CommandApi {
|
||||
Ok(BlobObject::create_and_deduplicate(&ctx, file, file)?.to_abs_path())
|
||||
}
|
||||
|
||||
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx.draft_self_report().await?.to_u32())
|
||||
}
|
||||
|
||||
/// Sets the given configuration key.
|
||||
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -879,38 +864,6 @@ impl CommandApi {
|
||||
Ok(chat_id.to_u32())
|
||||
}
|
||||
|
||||
/// Like `secure_join()`, but allows to pass a source and a UI-path.
|
||||
/// You only need this if your UI has an option to send statistics
|
||||
/// to Delta Chat's developers.
|
||||
///
|
||||
/// **source**: The source where the QR code came from.
|
||||
/// E.g. a link that was clicked inside or outside Delta Chat,
|
||||
/// the "Paste from Clipboard" action,
|
||||
/// the "Load QR code as image" action,
|
||||
/// or a QR code scan.
|
||||
///
|
||||
/// **uipath**: Which UI path did the user use to arrive at the QR code screen.
|
||||
/// If the SecurejoinSource was ExternalLink or InternalLink,
|
||||
/// pass `None` here, because the QR code screen wasn't even opened.
|
||||
/// ```
|
||||
async fn secure_join_with_ux_info(
|
||||
&self,
|
||||
account_id: u32,
|
||||
qr: String,
|
||||
source: Option<SecurejoinSource>,
|
||||
uipath: Option<SecurejoinUiPath>,
|
||||
) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = securejoin::join_securejoin_with_ux_info(
|
||||
&ctx,
|
||||
&qr,
|
||||
source.map(Into::into),
|
||||
uipath.map(Into::into),
|
||||
)
|
||||
.await?;
|
||||
Ok(chat_id.to_u32())
|
||||
}
|
||||
|
||||
async fn leave_group(&self, account_id: u32, chat_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
remove_contact_from_chat(&ctx, ChatId::new(chat_id), ContactId::SELF).await
|
||||
@@ -959,7 +912,7 @@ impl CommandApi {
|
||||
/// explicitly as it may happen that oneself gets removed from a still existing
|
||||
/// group
|
||||
///
|
||||
/// - for broadcast channels, all recipients are returned, DC_CONTACT_ID_SELF is not included
|
||||
/// - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
|
||||
///
|
||||
/// - for mailing lists, the behavior is not documented currently, we will decide on that later.
|
||||
/// for now, the UI should not show the list for mailing lists.
|
||||
@@ -978,7 +931,7 @@ impl CommandApi {
|
||||
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
|
||||
}
|
||||
|
||||
/// Create a new encrypted group chat (with key-contacts).
|
||||
/// Create a new group chat.
|
||||
///
|
||||
/// After creation,
|
||||
/// the group has one member with the ID DC_CONTACT_ID_SELF
|
||||
@@ -994,53 +947,32 @@ impl CommandApi {
|
||||
/// To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`.
|
||||
/// This may be useful if you want to show some help for just created groups.
|
||||
///
|
||||
/// `protect` argument is deprecated as of 2025-10-22 and is left for compatibility.
|
||||
/// Pass `false` here.
|
||||
async fn create_group_chat(
|
||||
&self,
|
||||
account_id: u32,
|
||||
name: String,
|
||||
_protect: bool,
|
||||
) -> Result<u32> {
|
||||
/// @param protect If set to 1 the function creates group with protection initially enabled.
|
||||
/// Only verified members are allowed in these groups
|
||||
/// and end-to-end-encryption is always enabled.
|
||||
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::create_group(&ctx, &name).await.map(|id| id.to_u32())
|
||||
}
|
||||
|
||||
/// Create a new unencrypted group chat.
|
||||
///
|
||||
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
|
||||
/// address-contacts.
|
||||
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::create_group_unencrypted(&ctx, &name)
|
||||
let protect = match protect {
|
||||
true => ProtectionStatus::Protected,
|
||||
false => ProtectionStatus::Unprotected,
|
||||
};
|
||||
chat::create_group_chat(&ctx, protect, &name)
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
}
|
||||
|
||||
/// Deprecated 2025-07 in favor of create_broadcast().
|
||||
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
||||
self.create_broadcast(account_id, "Channel".to_string())
|
||||
.await
|
||||
}
|
||||
|
||||
/// Create a new **broadcast channel**
|
||||
/// (called "Channel" in the UI).
|
||||
/// Create a new broadcast list.
|
||||
///
|
||||
/// Broadcast channels are similar to groups on the sending device,
|
||||
/// Broadcast lists 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.
|
||||
/// and will 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 [`CommandApi::create_group_chat`] for more information on the unpromoted state.
|
||||
///
|
||||
/// Returns the created chat's id.
|
||||
async fn create_broadcast(&self, account_id: u32, chat_name: String) -> Result<u32> {
|
||||
/// For historical reasons, this function does not take a name directly,
|
||||
/// instead you have to set the name using dc_set_chat_name()
|
||||
/// after creating the broadcast list.
|
||||
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::create_broadcast(&ctx, chat_name)
|
||||
chat::create_broadcast_list(&ctx)
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
}
|
||||
@@ -1084,7 +1016,7 @@ impl CommandApi {
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
visibility: JsonrpcChatVisibility,
|
||||
visibility: JSONRPCChatVisibility,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
@@ -1251,10 +1183,8 @@ impl CommandApi {
|
||||
}
|
||||
|
||||
/// Returns all messages of a particular chat.
|
||||
///
|
||||
/// * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
|
||||
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
|
||||
/// corresponding (following) day in the local timezone.
|
||||
/// If `add_daymarker` is `true`, it will return them as
|
||||
/// `DC_MSG_ID_DAYMARKER`, e.g. [1234, 1237, 9, 1239].
|
||||
async fn get_message_ids(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1289,7 +1219,7 @@ impl CommandApi {
|
||||
chat_id: u32,
|
||||
info_only: bool,
|
||||
add_daymarker: bool,
|
||||
) -> Result<Vec<JsonrpcMessageListItem>> {
|
||||
) -> Result<Vec<JSONRPCMessageListItem>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg = get_chat_msgs_ex(
|
||||
&ctx,
|
||||
@@ -1303,7 +1233,7 @@ impl CommandApi {
|
||||
Ok(msg
|
||||
.iter()
|
||||
.map(|chat_item| (*chat_item).into())
|
||||
.collect::<Vec<JsonrpcMessageListItem>>())
|
||||
.collect::<Vec<JSONRPCMessageListItem>>())
|
||||
}
|
||||
|
||||
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
|
||||
@@ -1497,14 +1427,7 @@ impl CommandApi {
|
||||
|
||||
/// Add a single contact as a result of an explicit user action.
|
||||
///
|
||||
/// This will always create or look up an address-contact,
|
||||
/// i.e. a contact identified by an email address,
|
||||
/// with all messages sent to and from this contact being unencrypted.
|
||||
/// If the user just clicked on an email address,
|
||||
/// you should first check [`Self::lookup_contact_id_by_addr`]/`lookupContactIdByAddr.`,
|
||||
/// and only if there is no contact yet, call this function here.
|
||||
///
|
||||
/// Returns contact id of the created or existing contact.
|
||||
/// Returns contact id of the created or existing contact
|
||||
async fn create_contact(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1556,14 +1479,6 @@ impl CommandApi {
|
||||
Ok(contacts)
|
||||
}
|
||||
|
||||
/// Returns ids of known and unblocked contacts.
|
||||
///
|
||||
/// By default, key-contacts are listed.
|
||||
///
|
||||
/// * `list_flags` - A combination of flags:
|
||||
/// - `DC_GCL_ADD_SELF` - Add SELF unless filtered by other parameters.
|
||||
/// - `DC_GCL_ADDRESS` - List address-contacts instead of key-contacts.
|
||||
/// * `query` - A string to filter the list.
|
||||
async fn get_contact_ids(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1575,10 +1490,8 @@ impl CommandApi {
|
||||
Ok(contacts.into_iter().map(|c| c.to_u32()).collect())
|
||||
}
|
||||
|
||||
/// Returns known and unblocked contacts.
|
||||
///
|
||||
/// Formerly called `getContacts2` in Desktop.
|
||||
/// See [`Self::get_contact_ids`] for parameters and more info.
|
||||
/// Get a list of contacts.
|
||||
/// (formerly called getContacts2 in desktop)
|
||||
async fn get_contacts(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1629,6 +1542,15 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resets contact encryption.
|
||||
async fn reset_contact_encryption(&self, account_id: u32, contact_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let contact_id = ContactId::new(contact_id);
|
||||
|
||||
contact_id.reset_encryption(&ctx).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets display name for existing contact.
|
||||
async fn change_contact_name(
|
||||
&self,
|
||||
@@ -1654,19 +1576,9 @@ impl CommandApi {
|
||||
Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await
|
||||
}
|
||||
|
||||
/// 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().
|
||||
async fn lookup_contact_id_by_addr(
|
||||
@@ -1822,13 +1734,13 @@ impl CommandApi {
|
||||
|
||||
/// Offers a backup for remote devices to retrieve.
|
||||
///
|
||||
/// Can be canceled by stopping the ongoing process. Success or failure can be tracked
|
||||
/// Can be cancelled by stopping the ongoing process. Success or failure can be tracked
|
||||
/// via the `ImexProgress` event which should either reach `1000` for success or `0` for
|
||||
/// failure.
|
||||
///
|
||||
/// This **stops IO** while it is running.
|
||||
///
|
||||
/// Returns once a remote device has retrieved the backup, or is canceled.
|
||||
/// Returns once a remote device has retrieved the backup, or is cancelled.
|
||||
async fn provide_backup(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
@@ -1894,7 +1806,7 @@ impl CommandApi {
|
||||
/// This retrieves the backup from a remote device over the network and imports it into
|
||||
/// the current device.
|
||||
///
|
||||
/// Can be canceled by stopping the ongoing process.
|
||||
/// Can be cancelled by stopping the ongoing process.
|
||||
///
|
||||
/// Do not forget to call start_io on the account after a successful import,
|
||||
/// otherwise it will not connect to the email server.
|
||||
@@ -1932,7 +1844,7 @@ impl CommandApi {
|
||||
/// If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
|
||||
async fn get_connectivity(&self, account_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx.get_connectivity() as u32)
|
||||
Ok(ctx.get_connectivity().await as u32)
|
||||
}
|
||||
|
||||
/// Get an overview of the current connectivity, and possibly more statistics.
|
||||
@@ -2015,11 +1927,6 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Leaves the gossip of the webxdc with the given message id.
|
||||
///
|
||||
/// NB: When this is called before closing a webxdc app in UIs, it must be guaranteed that
|
||||
/// `send_webxdc_realtime_*()` functions aren't called for the given `instance_message_id`
|
||||
/// anymore until the app is open again.
|
||||
async fn leave_webxdc_realtime(&self, account_id: u32, instance_message_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
leave_webxdc_realtime(&ctx, MsgId::new(instance_message_id)).await
|
||||
@@ -2097,53 +2004,6 @@ impl CommandApi {
|
||||
.map(|msg_id| msg_id.to_u32()))
|
||||
}
|
||||
|
||||
/// Starts an outgoing call.
|
||||
async fn place_outgoing_call(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
place_call_info: String,
|
||||
) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg_id = ctx
|
||||
.place_outgoing_call(ChatId::new(chat_id), place_call_info)
|
||||
.await?;
|
||||
Ok(msg_id.to_u32())
|
||||
}
|
||||
|
||||
/// Accepts an incoming call.
|
||||
async fn accept_incoming_call(
|
||||
&self,
|
||||
account_id: u32,
|
||||
msg_id: u32,
|
||||
accept_call_info: String,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.accept_incoming_call(MsgId::new(msg_id), accept_call_info)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ends incoming or outgoing call.
|
||||
async fn end_call(&self, account_id: u32, msg_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.end_call(MsgId::new(msg_id)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns information about the call.
|
||||
async fn call_info(&self, account_id: u32, msg_id: u32) -> Result<JsonrpcCallInfo> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let call_info = JsonrpcCallInfo::from_msg_id(&ctx, MsgId::new(msg_id)).await?;
|
||||
Ok(call_info)
|
||||
}
|
||||
|
||||
/// Returns JSON with ICE servers, to be used for WebRTC video calls.
|
||||
async fn ice_servers(&self, account_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ice_servers(&ctx).await
|
||||
}
|
||||
|
||||
/// Makes an HTTP GET request and returns a response.
|
||||
///
|
||||
/// `url` is the HTTP or HTTPS URL.
|
||||
@@ -2224,7 +2084,7 @@ impl CommandApi {
|
||||
&self,
|
||||
account_id: u32,
|
||||
message_id: u32,
|
||||
) -> Result<Option<JsonrpcReactions>> {
|
||||
) -> Result<Option<JSONRPCReactions>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let reactions = get_msg_reactions(&ctx, MsgId::new(message_id)).await?;
|
||||
if reactions.is_empty() {
|
||||
@@ -2295,6 +2155,13 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_videochat_invitation(&self, account_id: u32, chat_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::send_videochat_invitation(&ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// misc prototyping functions
|
||||
// that might get removed later again
|
||||
@@ -2325,7 +2192,8 @@ impl CommandApi {
|
||||
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
ensure!(
|
||||
message.get_viewtype() == Viewtype::Sticker,
|
||||
"message {msg_id} is not a sticker"
|
||||
"message {} is not a sticker",
|
||||
msg_id
|
||||
);
|
||||
let account_folder = ctx
|
||||
.get_dbfile()
|
||||
@@ -2545,7 +2413,10 @@ impl CommandApi {
|
||||
.to_u32();
|
||||
Ok(msg_id)
|
||||
} else {
|
||||
Err(anyhow!("chat with id {chat_id} doesn't have draft message"))
|
||||
Err(anyhow!(
|
||||
"chat with id {} doesn't have draft message",
|
||||
chat_id
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use deltachat::calls::{call_state, sdp_has_video, 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 SDP offer has a video.
|
||||
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 = sdp_has_video(&sdp_offer).unwrap_or_default();
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -19,29 +19,17 @@ pub struct FullChat {
|
||||
id: u32,
|
||||
name: String,
|
||||
|
||||
/// 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.
|
||||
/// True if the chat is protected.
|
||||
///
|
||||
/// 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,
|
||||
/// UI should display a green checkmark
|
||||
/// in the chat title,
|
||||
/// in the chat profile title and
|
||||
/// in the chatlist item
|
||||
/// if chat protection is enabled.
|
||||
/// UI should also display a green checkmark
|
||||
/// in the contact profile
|
||||
/// if 1:1 chat with this contact exists and is protected.
|
||||
is_protected: bool,
|
||||
profile_image: Option<String>, //BLOBS ?
|
||||
archived: bool,
|
||||
pinned: bool,
|
||||
@@ -59,15 +47,8 @@ pub struct FullChat {
|
||||
fresh_message_counter: usize,
|
||||
// is_group - please check over chat.type in frontend instead
|
||||
is_contact_request: bool,
|
||||
|
||||
is_protection_broken: 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,
|
||||
is_muted: bool,
|
||||
ephemeral_timer: u32, //TODO look if there are more important properties in newer core versions
|
||||
@@ -126,7 +107,7 @@ impl FullChat {
|
||||
Ok(FullChat {
|
||||
id: chat_id,
|
||||
name: chat.name.clone(),
|
||||
is_encrypted: chat.is_encrypted(context).await?,
|
||||
is_protected: chat.is_protected(),
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||
@@ -139,6 +120,7 @@ impl FullChat {
|
||||
color,
|
||||
fresh_message_counter,
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
is_protection_broken: chat.is_protection_broken(),
|
||||
is_device_chat: chat.is_device_talk(),
|
||||
self_in_group: contact_ids.contains(&ContactId::SELF),
|
||||
is_muted: chat.is_muted(),
|
||||
@@ -166,29 +148,17 @@ pub struct BasicChat {
|
||||
id: u32,
|
||||
name: String,
|
||||
|
||||
/// 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.
|
||||
/// True if the chat is protected.
|
||||
///
|
||||
/// 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,
|
||||
/// UI should display a green checkmark
|
||||
/// in the chat title,
|
||||
/// in the chat profile title and
|
||||
/// in the chatlist item
|
||||
/// if chat protection is enabled.
|
||||
/// UI should also display a green checkmark
|
||||
/// in the contact profile
|
||||
/// if 1:1 chat with this contact exists and is protected.
|
||||
is_protected: bool,
|
||||
profile_image: Option<String>, //BLOBS ?
|
||||
archived: bool,
|
||||
pinned: bool,
|
||||
@@ -197,7 +167,7 @@ pub struct BasicChat {
|
||||
is_self_talk: bool,
|
||||
color: String,
|
||||
is_contact_request: bool,
|
||||
|
||||
is_protection_broken: bool,
|
||||
is_device_chat: bool,
|
||||
is_muted: bool,
|
||||
}
|
||||
@@ -216,7 +186,7 @@ impl BasicChat {
|
||||
Ok(BasicChat {
|
||||
id: chat_id,
|
||||
name: chat.name.clone(),
|
||||
is_encrypted: chat.is_encrypted(context).await?,
|
||||
is_protected: chat.is_protected(),
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||
@@ -225,6 +195,7 @@ impl BasicChat {
|
||||
is_self_talk: chat.is_self_talk(),
|
||||
color,
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
is_protection_broken: chat.is_protection_broken(),
|
||||
is_device_chat: chat.is_device_talk(),
|
||||
is_muted: chat.is_muted(),
|
||||
})
|
||||
@@ -259,18 +230,18 @@ impl MuteDuration {
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "ChatVisibility")]
|
||||
pub enum JsonrpcChatVisibility {
|
||||
pub enum JSONRPCChatVisibility {
|
||||
Normal,
|
||||
Archived,
|
||||
Pinned,
|
||||
}
|
||||
|
||||
impl JsonrpcChatVisibility {
|
||||
impl JSONRPCChatVisibility {
|
||||
pub fn into_core_type(self) -> ChatVisibility {
|
||||
match self {
|
||||
JsonrpcChatVisibility::Normal => ChatVisibility::Normal,
|
||||
JsonrpcChatVisibility::Archived => ChatVisibility::Archived,
|
||||
JsonrpcChatVisibility::Pinned => ChatVisibility::Pinned,
|
||||
JSONRPCChatVisibility::Normal => ChatVisibility::Normal,
|
||||
JSONRPCChatVisibility::Archived => ChatVisibility::Archived,
|
||||
JSONRPCChatVisibility::Pinned => ChatVisibility::Pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::{Context, Result};
|
||||
use deltachat::chat::{Chat, ChatId};
|
||||
use deltachat::chatlist::get_last_message_for_chat;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::contact::{Contact, ContactId};
|
||||
use deltachat::{
|
||||
chat::{get_chat_contacts, ChatVisibility},
|
||||
chatlist::Chatlist,
|
||||
@@ -23,38 +23,13 @@ pub enum ChatListItemFetchResult {
|
||||
name: String,
|
||||
avatar_path: Option<String>,
|
||||
color: String,
|
||||
chat_type: u32,
|
||||
last_updated: Option<i64>,
|
||||
summary_text1: String,
|
||||
summary_text2: String,
|
||||
summary_status: u32,
|
||||
/// showing preview if last chat message is image
|
||||
summary_preview_image: Option<String>,
|
||||
|
||||
/// 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_protected: bool,
|
||||
is_group: bool,
|
||||
fresh_message_counter: usize,
|
||||
is_self_talk: bool,
|
||||
@@ -65,6 +40,8 @@ pub enum ChatListItemFetchResult {
|
||||
is_pinned: bool,
|
||||
is_muted: 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)
|
||||
dm_chat_contact: Option<u32>,
|
||||
was_seen_recently: bool,
|
||||
@@ -126,8 +103,11 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
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 chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
||||
let contact = chat_contacts.first();
|
||||
let was_seen_recently = match contact {
|
||||
Some(contact) => Contact::get_by_id(ctx, *contact)
|
||||
@@ -151,23 +131,23 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
name: chat.get_name().to_owned(),
|
||||
avatar_path,
|
||||
color,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
last_updated,
|
||||
summary_text1,
|
||||
summary_text2,
|
||||
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
|
||||
summary_preview_image,
|
||||
is_encrypted: chat.is_encrypted(ctx).await?,
|
||||
is_protected: chat.is_protected(),
|
||||
is_group: chat.get_type() == Chattype::Group,
|
||||
fresh_message_counter,
|
||||
is_self_talk: chat.is_self_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_archived: visibility == ChatVisibility::Archived,
|
||||
is_pinned: visibility == ChatVisibility::Pinned,
|
||||
is_muted: chat.is_muted(),
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
is_broadcast: chat.get_type() == Chattype::Broadcast,
|
||||
dm_chat_contact,
|
||||
was_seen_recently,
|
||||
last_message_type: message_type,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use deltachat::color;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::key::{DcKey, SignedPublicKey};
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
@@ -19,48 +19,29 @@ pub struct ContactObject {
|
||||
profile_image: Option<String>, // BLOBS
|
||||
name_and_addr: String,
|
||||
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,
|
||||
|
||||
/// True if the contact
|
||||
/// can be added to protected chats
|
||||
/// because SELF and contact have verified their fingerprints in both directions.
|
||||
/// True if the contact can be added to verified groups.
|
||||
///
|
||||
/// 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,
|
||||
|
||||
/// The contact ID that verified a contact.
|
||||
/// True if the contact profile title should have a green checkmark.
|
||||
///
|
||||
/// As verifier may be unknown,
|
||||
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
|
||||
/// This indicates whether 1:1 chat has a green checkmark
|
||||
/// 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 `verifierId` != 0,
|
||||
/// display text "Introduced by ..."
|
||||
/// with the name and address of the contact
|
||||
/// formatted by `name_and_addr`/`nameAndAddr`.
|
||||
/// 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.
|
||||
/// If this is present,
|
||||
/// display a green checkmark and "Introduced by ..."
|
||||
/// string followed by the verifier contact name and address
|
||||
/// in the contact profile.
|
||||
verifier_id: Option<u32>,
|
||||
|
||||
/// the contact's last seen timestamp
|
||||
@@ -81,11 +62,11 @@ impl ContactObject {
|
||||
None => None,
|
||||
};
|
||||
let is_verified = contact.is_verified(context).await?;
|
||||
let is_profile_verified = contact.is_profile_verified(context).await?;
|
||||
|
||||
let verifier_id = contact
|
||||
.get_verifier_id(context)
|
||||
.await?
|
||||
.flatten()
|
||||
.map(|contact_id| contact_id.to_u32());
|
||||
|
||||
Ok(ContactObject {
|
||||
@@ -99,9 +80,9 @@ impl ContactObject {
|
||||
profile_image, //BLOBS
|
||||
name_and_addr: contact.get_name_n_addr(),
|
||||
is_blocked: contact.is_blocked(),
|
||||
is_key_contact: contact.is_key_contact(),
|
||||
e2ee_avail: contact.e2ee_avail(context).await?,
|
||||
is_verified,
|
||||
is_profile_verified,
|
||||
verifier_id,
|
||||
last_seen: contact.last_seen(),
|
||||
was_seen_recently: contact.was_seen_recently(),
|
||||
@@ -130,13 +111,7 @@ pub struct VcardContact {
|
||||
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
|
||||
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
|
||||
let display_name = vc.display_name().to_string();
|
||||
let is_self = false;
|
||||
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);
|
||||
let color = color::str_to_color(&vc.addr.to_lowercase());
|
||||
Self {
|
||||
addr: vc.addr,
|
||||
display_name,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use deltachat::{Event as CoreEvent, EventType as CoreEventType};
|
||||
use num_traits::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
@@ -225,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.
|
||||
/// Or the verify state of a chat has changed.
|
||||
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
||||
/// and removeContactFromChat().
|
||||
///
|
||||
@@ -294,8 +294,8 @@ pub enum EventType {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ImexFileWritten { path: String },
|
||||
|
||||
/// Progress event sent when SecureJoin protocol has finished
|
||||
/// from the view of the inviter (Alice, the person who shows the QR code).
|
||||
/// Progress information of a secure-join handshake from the view of the inviter
|
||||
/// (Alice, the person who shows the QR code).
|
||||
///
|
||||
/// These events are typically sent after a joiner has scanned the QR code
|
||||
/// generated by getChatSecurejoinQrCodeSvg().
|
||||
@@ -304,14 +304,11 @@ pub enum EventType {
|
||||
/// ID of the contact that wants to join.
|
||||
contact_id: u32,
|
||||
|
||||
/// The type of the joined chat.
|
||||
/// This can take the same values
|
||||
/// as `BasicChat.chatType` ([`crate::api::types::chat::BasicChat::chat_type`]).
|
||||
chat_type: u32,
|
||||
/// ID of the chat in case of success.
|
||||
chat_id: u32,
|
||||
|
||||
/// Progress, always 1000.
|
||||
/// Progress as:
|
||||
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
|
||||
/// 1000=Protocol finished for this contact.
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
@@ -420,45 +417,6 @@ pub enum EventType {
|
||||
/// Number of events skipped.
|
||||
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,
|
||||
},
|
||||
|
||||
/// 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,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<CoreEventType> for EventType {
|
||||
@@ -565,13 +523,9 @@ impl From<CoreEventType> for EventType {
|
||||
},
|
||||
CoreEventType::SecurejoinInviterProgress {
|
||||
contact_id,
|
||||
chat_type,
|
||||
chat_id,
|
||||
progress,
|
||||
} => SecurejoinInviterProgress {
|
||||
contact_id: contact_id.to_u32(),
|
||||
chat_type: chat_type.to_u32().unwrap_or(0),
|
||||
chat_id: chat_id.to_u32(),
|
||||
progress,
|
||||
},
|
||||
CoreEventType::SecurejoinJoinerProgress {
|
||||
@@ -613,34 +567,6 @@ impl From<CoreEventType> for EventType {
|
||||
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
||||
CoreEventType::AccountsChanged => AccountsChanged,
|
||||
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 } => IncomingCallAccepted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
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(),
|
||||
},
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
|
||||
@@ -18,7 +18,7 @@ use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
use super::contact::ContactObject;
|
||||
use super::reactions::JsonrpcReactions;
|
||||
use super::reactions::JSONRPCReactions;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||
@@ -59,13 +59,6 @@ pub struct MessageObject {
|
||||
|
||||
// summary - use/create another function if you need it
|
||||
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,
|
||||
is_setupmessage: bool,
|
||||
is_info: bool,
|
||||
@@ -84,6 +77,9 @@ pub struct MessageObject {
|
||||
dimensions_height: i32,
|
||||
dimensions_width: i32,
|
||||
|
||||
videochat_type: Option<u32>,
|
||||
videochat_url: Option<String>,
|
||||
|
||||
override_sender_name: Option<String>,
|
||||
sender: ContactObject,
|
||||
|
||||
@@ -102,7 +98,7 @@ pub struct MessageObject {
|
||||
|
||||
saved_message_id: Option<u32>,
|
||||
|
||||
reactions: Option<JsonrpcReactions>,
|
||||
reactions: Option<JSONRPCReactions>,
|
||||
|
||||
vcard_contact: Option<VcardContact>,
|
||||
}
|
||||
@@ -236,6 +232,15 @@ impl MessageObject {
|
||||
dimensions_height: message.get_height(),
|
||||
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,
|
||||
sender,
|
||||
|
||||
@@ -309,8 +314,8 @@ pub enum MessageViewtype {
|
||||
/// Message containing any file, eg. a PDF.
|
||||
File,
|
||||
|
||||
/// Message is a call.
|
||||
Call,
|
||||
/// Message is an invitation to a videochat.
|
||||
VideochatInvitation,
|
||||
|
||||
/// Message is an webxdc instance.
|
||||
Webxdc,
|
||||
@@ -333,7 +338,7 @@ impl From<Viewtype> for MessageViewtype {
|
||||
Viewtype::Voice => MessageViewtype::Voice,
|
||||
Viewtype::Video => MessageViewtype::Video,
|
||||
Viewtype::File => MessageViewtype::File,
|
||||
Viewtype::Call => MessageViewtype::Call,
|
||||
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
|
||||
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
||||
Viewtype::Vcard => MessageViewtype::Vcard,
|
||||
}
|
||||
@@ -352,7 +357,7 @@ impl From<MessageViewtype> for Viewtype {
|
||||
MessageViewtype::Voice => Viewtype::Voice,
|
||||
MessageViewtype::Video => Viewtype::Video,
|
||||
MessageViewtype::File => Viewtype::File,
|
||||
MessageViewtype::Call => Viewtype::Call,
|
||||
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
|
||||
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
||||
MessageViewtype::Vcard => Viewtype::Vcard,
|
||||
}
|
||||
@@ -404,9 +409,6 @@ pub enum SystemMessageType {
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged,
|
||||
|
||||
// Chat is e2ee
|
||||
ChatE2ee,
|
||||
|
||||
// Chat protection state changed
|
||||
ChatProtectionEnabled,
|
||||
ChatProtectionDisabled,
|
||||
@@ -425,9 +427,6 @@ pub enum SystemMessageType {
|
||||
|
||||
/// This message contains a users iroh node address.
|
||||
IrohNodeAddr,
|
||||
|
||||
CallAccepted,
|
||||
CallEnded,
|
||||
}
|
||||
|
||||
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
@@ -444,7 +443,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
|
||||
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
|
||||
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
|
||||
SystemMessage::ChatE2ee => SystemMessageType::ChatE2ee,
|
||||
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
|
||||
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
|
||||
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
||||
@@ -454,8 +452,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
|
||||
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
|
||||
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
|
||||
SystemMessage::CallAccepted => SystemMessageType::CallAccepted,
|
||||
SystemMessage::CallEnded => SystemMessageType::CallEnded,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -532,6 +528,7 @@ pub struct MessageSearchResult {
|
||||
chat_color: String,
|
||||
chat_name: String,
|
||||
chat_type: u32,
|
||||
is_chat_protected: bool,
|
||||
is_chat_contact_request: bool,
|
||||
is_chat_archived: bool,
|
||||
message: String,
|
||||
@@ -571,6 +568,7 @@ impl MessageSearchResult {
|
||||
chat_color,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
chat_profile_image,
|
||||
is_chat_protected: chat.is_protected(),
|
||||
is_chat_contact_request: chat.is_contact_request(),
|
||||
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
|
||||
message: message.get_text(),
|
||||
@@ -581,7 +579,7 @@ impl MessageSearchResult {
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
|
||||
pub enum JsonrpcMessageListItem {
|
||||
pub enum JSONRPCMessageListItem {
|
||||
Message {
|
||||
msg_id: u32,
|
||||
},
|
||||
@@ -594,13 +592,13 @@ pub enum JsonrpcMessageListItem {
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ChatItem> for JsonrpcMessageListItem {
|
||||
impl From<ChatItem> for JSONRPCMessageListItem {
|
||||
fn from(item: ChatItem) -> Self {
|
||||
match item {
|
||||
ChatItem::Message { msg_id } => JsonrpcMessageListItem::Message {
|
||||
ChatItem::Message { msg_id } => JSONRPCMessageListItem::Message {
|
||||
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 calls;
|
||||
pub mod chat;
|
||||
pub mod chat_list;
|
||||
pub mod contact;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use deltachat::qr::Qr;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
@@ -226,6 +225,13 @@ impl From<Qr> for QrObject {
|
||||
auth_token,
|
||||
},
|
||||
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::Addr { contact_id, draft } => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
@@ -305,53 +311,3 @@ impl From<Qr> for QrObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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.
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Reaction", rename_all = "camelCase")]
|
||||
pub struct JsonrpcReaction {
|
||||
pub struct JSONRPCReaction {
|
||||
/// Emoji.
|
||||
emoji: String,
|
||||
|
||||
@@ -22,14 +22,14 @@ pub struct JsonrpcReaction {
|
||||
/// Structure representing all reactions to a particular message.
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Reactions", rename_all = "camelCase")]
|
||||
pub struct JsonrpcReactions {
|
||||
pub struct JSONRPCReactions {
|
||||
/// Map from a contact to it's reaction to message.
|
||||
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
||||
/// 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 {
|
||||
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
|
||||
|
||||
@@ -56,7 +56,7 @@ impl From<Reactions> for JsonrpcReactions {
|
||||
false
|
||||
};
|
||||
|
||||
let reaction = JsonrpcReaction {
|
||||
let reaction = JSONRPCReaction {
|
||||
emoji,
|
||||
count,
|
||||
is_from_self,
|
||||
@@ -64,7 +64,7 @@ impl From<Reactions> for JsonrpcReactions {
|
||||
reactions_v.push(reaction)
|
||||
}
|
||||
|
||||
JsonrpcReactions {
|
||||
JSONRPCReactions {
|
||||
reactions_by_contact,
|
||||
reactions: reactions_v,
|
||||
}
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.22.0"
|
||||
"version": "1.160.0"
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export class BaseDeltaChat<
|
||||
Transport extends BaseTransport<any>,
|
||||
> extends TinyEmitter<Events> {
|
||||
rpc: RawClient;
|
||||
account?: T.Account;
|
||||
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
||||
|
||||
//@ts-ignore
|
||||
@@ -35,10 +36,6 @@ export class BaseDeltaChat<
|
||||
|
||||
constructor(
|
||||
public transport: Transport,
|
||||
/**
|
||||
* Whether to start calling {@linkcode RawClient.getNextEvent}
|
||||
* and emitting the respective events on this class.
|
||||
*/
|
||||
startEventLoop: boolean,
|
||||
) {
|
||||
super();
|
||||
@@ -48,9 +45,6 @@ export class BaseDeltaChat<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see the constructor's `startEventLoop`
|
||||
*/
|
||||
async eventLoop(): Promise<void> {
|
||||
while (true) {
|
||||
const event = await this.rpc.getNextEvent();
|
||||
@@ -69,17 +63,10 @@ export class BaseDeltaChat<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@linkcode BaseDeltaChat.rpc.getAllAccounts} instead.
|
||||
*/
|
||||
async listAccounts(): Promise<T.Account[]> {
|
||||
return await this.rpc.getAllAccounts();
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience function to listen on events binned by `account_id`
|
||||
* (see {@linkcode RawClient.getAllAccounts}).
|
||||
*/
|
||||
getContextEvents(account_id: number) {
|
||||
if (this.contextEmitters[account_id]) {
|
||||
return this.contextEmitters[account_id];
|
||||
|
||||
@@ -95,10 +95,8 @@ describe("online tests", function () {
|
||||
false,
|
||||
);
|
||||
|
||||
// There are 2 messages in the chat:
|
||||
// 'Messages are end-to-end encrypted' (info message) and 'Hello'
|
||||
expect(messageList).have.length(2);
|
||||
const message = await dc.rpc.getMessage(accountId2, messageList[1]);
|
||||
expect(messageList).have.length(1);
|
||||
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
|
||||
expect(message.text).equal("Hello");
|
||||
expect(message.showPadlock).equal(true);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.22.0"
|
||||
version = "1.160.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -6,7 +6,9 @@ use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
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::constants::*;
|
||||
use deltachat::contact::*;
|
||||
@@ -18,6 +20,7 @@ use deltachat::log::LogExt;
|
||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use deltachat::mimeparser::SystemMessage;
|
||||
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::qr_code_generator::create_qr_svg;
|
||||
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.
|
||||
async fn reset_tables(context: &Context, bits: i32) {
|
||||
println!("Resetting tables ({bits})...");
|
||||
if 0 != bits & 2 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM acpeerstates;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(2) Peerstates reset.");
|
||||
}
|
||||
if 0 != bits & 4 {
|
||||
context
|
||||
.sql()
|
||||
@@ -85,7 +96,7 @@ async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||
let data = read_file(context, filename).await?;
|
||||
|
||||
if let Err(err) = receive_imf(context, &data, false).await {
|
||||
eprintln!("receive_imf errored: {err:?}");
|
||||
println!("receive_imf errored: {err:?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -208,7 +219,13 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
} 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 {
|
||||
Ok(info) => format!(
|
||||
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
|
||||
@@ -260,7 +277,7 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
|
||||
|
||||
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
|
||||
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 name = contact.get_display_name();
|
||||
let addr = contact.get_addr();
|
||||
@@ -279,6 +296,15 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
|
||||
verified_str,
|
||||
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);
|
||||
}
|
||||
@@ -316,7 +342,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
send-backup\n\
|
||||
receive-backup <qr>\n\
|
||||
export-keys\n\
|
||||
import-keys <key-file>\n\
|
||||
import-keys\n\
|
||||
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
||||
reset <flags>\n\
|
||||
stop\n\
|
||||
@@ -325,6 +351,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
_ => println!(
|
||||
"==========================Database commands==\n\
|
||||
info\n\
|
||||
open <file to open or create>\n\
|
||||
close\n\
|
||||
set <configuration-key> [<value>]\n\
|
||||
get <configuration-key>\n\
|
||||
oauth2\n\
|
||||
@@ -339,30 +367,28 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
==============================Chat commands==\n\
|
||||
listchats [<query>]\n\
|
||||
listarchived\n\
|
||||
start-realtime <msg-id>\n\
|
||||
send-realtime <msg-id> <data>\n\
|
||||
chat [<chat-id>|0]\n\
|
||||
createchat <contact-id>\n\
|
||||
creategroup <name>\n\
|
||||
createbroadcast <name>\n\
|
||||
createbroadcast\n\
|
||||
createprotected <name>\n\
|
||||
addmember <contact-id>\n\
|
||||
removemember <contact-id>\n\
|
||||
groupname <name>\n\
|
||||
groupimage <image>\n\
|
||||
groupimage [<file>]\n\
|
||||
chatinfo\n\
|
||||
sendlocations <seconds>\n\
|
||||
setlocation <lat> <lng>\n\
|
||||
dellocations\n\
|
||||
getlocations [<contact-id>]\n\
|
||||
send <text>\n\
|
||||
send-sync <text>\n\
|
||||
sendempty\n\
|
||||
sendimage <file> [<text>]\n\
|
||||
sendsticker <file> [<text>]\n\
|
||||
sendfile <file> [<text>]\n\
|
||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||
sendsyncmsg\n\
|
||||
sendupdate <msg-id> <json status update>\n\
|
||||
videochat\n\
|
||||
draft [<text>]\n\
|
||||
devicemsg <text>\n\
|
||||
listmedia\n\
|
||||
@@ -374,7 +400,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
unmute <chat-id>\n\
|
||||
delchat <chat-id>\n\
|
||||
accept <chat-id>\n\
|
||||
blockchat <chat-id>\n\
|
||||
decline <chat-id>\n\
|
||||
===========================Message commands==\n\
|
||||
listmsgs <query>\n\
|
||||
msginfo <msg-id>\n\
|
||||
@@ -388,14 +414,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
react <msg-id> [<reaction>]\n\
|
||||
===========================Contact commands==\n\
|
||||
listcontacts [<query>]\n\
|
||||
listverified [<query>]\n\
|
||||
addcontact [<name>] <addr>\n\
|
||||
contactinfo <contact-id>\n\
|
||||
delcontact <contact-id>\n\
|
||||
cleanupcontacts\n\
|
||||
block <contact-id>\n\
|
||||
unblock <contact-id>\n\
|
||||
listblocked\n\
|
||||
import-vcard <file>\n\
|
||||
make-vcard <file> <contact-id> [contact-id ...]\n\
|
||||
======================================Misc.==\n\
|
||||
getqr [<chat-id>]\n\
|
||||
getqrsvg [<chat-id>]\n\
|
||||
@@ -416,7 +442,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Ok(setup_code) => {
|
||||
println!("Setup code for the transferred setup message: {setup_code}",)
|
||||
}
|
||||
Err(err) => bail!("Failed to generate setup code: {err}"),
|
||||
Err(err) => bail!("Failed to generate setup code: {}", err),
|
||||
},
|
||||
"get-setupcodebegin" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
@@ -430,7 +456,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
setupcodebegin.unwrap_or_default(),
|
||||
);
|
||||
} else {
|
||||
bail!("{msg_id} is no setup message.",);
|
||||
bail!("{} is no setup message.", msg_id,);
|
||||
}
|
||||
}
|
||||
"continue-key-transfer" => {
|
||||
@@ -482,17 +508,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("Exported to {}.", dir.to_string_lossy());
|
||||
}
|
||||
"import-keys" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <key-file> missing.");
|
||||
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
|
||||
}
|
||||
"poke" => {
|
||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||
}
|
||||
"reset" => {
|
||||
ensure!(
|
||||
!arg1.is_empty(),
|
||||
"Argument <bits> missing: 4=private keys, 8=rest but server config"
|
||||
);
|
||||
ensure!(!arg1.is_empty(), "Argument <bits> missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config");
|
||||
let bits: i32 = arg1.parse()?;
|
||||
ensure!(bits < 16, "<bits> must be lower than 16.");
|
||||
reset_tables(&context, bits).await;
|
||||
@@ -525,7 +547,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("Report written to: {file:#?}");
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get connectivity html: {err}");
|
||||
bail!("Failed to get connectivity html: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -560,7 +582,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
for i in (0..cnt).rev() {
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
|
||||
println!(
|
||||
"{}#{}: {} [{} fresh] {}{}{}",
|
||||
"{}#{}: {} [{} fresh] {}{}{}{}",
|
||||
chat_prefix(&chat),
|
||||
chat.get_id(),
|
||||
chat.get_name(),
|
||||
@@ -571,6 +593,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ChatVisibility::Archived => "📦",
|
||||
ChatVisibility::Pinned => "📌",
|
||||
},
|
||||
if chat.is_protected() { "🛡️" } else { "" },
|
||||
if chat.is_contact_request() {
|
||||
"🆕"
|
||||
} else {
|
||||
@@ -613,7 +636,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{cnt} chats");
|
||||
eprintln!("{time_needed:?} to create this list");
|
||||
println!("{time_needed:?} to create this list");
|
||||
}
|
||||
"start-realtime" => {
|
||||
if arg1.is_empty() {
|
||||
@@ -685,7 +708,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
format!("{} member(s)", members.len())
|
||||
};
|
||||
println!(
|
||||
"{}#{}: {} [{}]{}{}{}",
|
||||
"{}#{}: {} [{}]{}{}{} {}",
|
||||
chat_prefix(sel_chat),
|
||||
sel_chat.get_id(),
|
||||
sel_chat.get_name(),
|
||||
@@ -703,6 +726,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
},
|
||||
_ => "".to_string(),
|
||||
},
|
||||
if sel_chat.is_protected() {
|
||||
"🛡️"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
);
|
||||
log_msglist(&context, &msglist).await?;
|
||||
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
|
||||
@@ -718,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?;
|
||||
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."
|
||||
);
|
||||
}
|
||||
@@ -731,16 +759,23 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"creategroup" => {
|
||||
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.");
|
||||
}
|
||||
"createbroadcast" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
let chat_id = chat::create_broadcast(&context, arg1.to_string()).await?;
|
||||
let chat_id = chat::create_broadcast_list(&context).await?;
|
||||
|
||||
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" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected");
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
@@ -892,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?;
|
||||
}
|
||||
"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" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
|
||||
@@ -956,6 +974,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
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" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
||||
|
||||
@@ -977,7 +999,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
},
|
||||
query,
|
||||
);
|
||||
eprintln!("{time_needed:?} to create this list");
|
||||
println!("{time_needed:?} to create this list");
|
||||
}
|
||||
"draft" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
@@ -1143,10 +1165,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"listcontacts" | "contacts" => {
|
||||
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!("{} key 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());
|
||||
println!("{} contacts.", contacts.len());
|
||||
}
|
||||
"addcontact" => {
|
||||
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
|
||||
@@ -1210,24 +1229,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
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" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
let qr = check_qr(&context, arg1).await?;
|
||||
@@ -1237,7 +1238,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
match set_config_from_qr(&context, arg1).await {
|
||||
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" => {
|
||||
@@ -1249,7 +1250,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"providerinfo" => {
|
||||
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) => {
|
||||
println!("Information for provider belonging to {arg1}:");
|
||||
println!("status: {}", info.status as u32);
|
||||
@@ -1285,7 +1289,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(())
|
||||
|
||||
@@ -179,11 +179,9 @@ const DB_COMMANDS: [&str; 11] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 39] = [
|
||||
const CHAT_COMMANDS: [&str; 36] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"start-realtime",
|
||||
"send-realtime",
|
||||
"chat",
|
||||
"createchat",
|
||||
"creategroup",
|
||||
@@ -199,16 +197,13 @@ const CHAT_COMMANDS: [&str; 39] = [
|
||||
"dellocations",
|
||||
"getlocations",
|
||||
"send",
|
||||
"send-sync",
|
||||
"sendempty",
|
||||
"sendimage",
|
||||
"sendsticker",
|
||||
"sendfile",
|
||||
"sendhtml",
|
||||
"sendsyncmsg",
|
||||
"sendupdate",
|
||||
"videochat",
|
||||
"draft",
|
||||
"devicemsg",
|
||||
"listmedia",
|
||||
"archive",
|
||||
"unarchive",
|
||||
@@ -216,48 +211,47 @@ const CHAT_COMMANDS: [&str; 39] = [
|
||||
"unpin",
|
||||
"mute",
|
||||
"unmute",
|
||||
"protect",
|
||||
"unprotect",
|
||||
"delchat",
|
||||
"accept",
|
||||
"blockchat",
|
||||
];
|
||||
const MESSAGE_COMMANDS: [&str; 10] = [
|
||||
const MESSAGE_COMMANDS: [&str; 9] = [
|
||||
"listmsgs",
|
||||
"msginfo",
|
||||
"download",
|
||||
"html",
|
||||
"listfresh",
|
||||
"forward",
|
||||
"resend",
|
||||
"markseen",
|
||||
"delmsg",
|
||||
"download",
|
||||
"react",
|
||||
];
|
||||
const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"listcontacts",
|
||||
"listverified",
|
||||
"addcontact",
|
||||
"contactinfo",
|
||||
"delcontact",
|
||||
"cleanupcontacts",
|
||||
"block",
|
||||
"unblock",
|
||||
"listblocked",
|
||||
"import-vcard",
|
||||
"make-vcard",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 14] = [
|
||||
const MISC_COMMANDS: [&str; 12] = [
|
||||
"getqr",
|
||||
"getqrsvg",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"joinqr",
|
||||
"setqr",
|
||||
"createqrsvg",
|
||||
"providerinfo",
|
||||
"fileinfo",
|
||||
"estimatedeletion",
|
||||
"clear",
|
||||
"exit",
|
||||
"quit",
|
||||
"help",
|
||||
"estimatedeletion",
|
||||
];
|
||||
|
||||
impl Hinter for DcHelper {
|
||||
@@ -313,7 +307,7 @@ impl Validator for DcHelper {}
|
||||
|
||||
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
if args.len() < 2 {
|
||||
eprintln!("Error: Bad arguments, expected [db-name].");
|
||||
println!("Error: Bad arguments, expected [db-name].");
|
||||
bail!("No db-name specified");
|
||||
}
|
||||
let context = ContextBuilder::new(args[1].clone().into())
|
||||
@@ -368,7 +362,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
false
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Error: {err:#}");
|
||||
println!("Error: {err:#}");
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -383,7 +377,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Error: {err:#}");
|
||||
println!("Error: {err:#}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -467,7 +461,7 @@ async fn handle_cmd(
|
||||
println!("QR code svg written to: {file:#?}");
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get QR code svg: {err}");
|
||||
bail!("Failed to get QR code svg: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.22.0"
|
||||
version = "1.160.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -19,7 +19,6 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from warnings import warn
|
||||
@@ -186,21 +185,7 @@ class Account:
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||
"""Looks up a known and unblocked contact with a given e-mail address.
|
||||
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()."""
|
||||
"""Check if an e-mail address belongs to a known and unblocked contact."""
|
||||
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||
return contact_id and Contact(self, contact_id)
|
||||
|
||||
@@ -300,44 +285,12 @@ class Account:
|
||||
chats.append(AttrDict(item))
|
||||
return chats
|
||||
|
||||
def create_group(self, name: str) -> Chat:
|
||||
def create_group(self, name: str, protect: bool = False) -> Chat:
|
||||
"""Create a new group chat.
|
||||
|
||||
After creation,
|
||||
the group has only self-contact as member 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.
|
||||
After creation, the group has only self-contact as member and is in unpromoted state.
|
||||
"""
|
||||
return Chat(self, self._rpc.create_group_chat(self.id, name, False))
|
||||
|
||||
def create_broadcast(self, name: str) -> Chat:
|
||||
"""Create a new **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))
|
||||
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
||||
|
||||
def get_chat_by_id(self, chat_id: int) -> Chat:
|
||||
"""Return the Chat instance with the given ID."""
|
||||
@@ -467,8 +420,3 @@ class Account:
|
||||
def initiate_autocrypt_key_transfer(self) -> None:
|
||||
"""Send Autocrypt Setup Message."""
|
||||
return self._rpc.initiate_autocrypt_key_transfer(self.id)
|
||||
|
||||
def ice_servers(self) -> list:
|
||||
"""Return ICE servers for WebRTC configuration."""
|
||||
ice_servers_json = self._rpc.ice_servers(self.id)
|
||||
return json.loads(ice_servers_json)
|
||||
|
||||
@@ -168,11 +168,6 @@ class Chat:
|
||||
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||
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:
|
||||
"""Forward a list of messages to this chat."""
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
@@ -294,8 +289,3 @@ class Chat:
|
||||
f.write(vcard.encode())
|
||||
f.flush()
|
||||
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
|
||||
|
||||
def place_outgoing_call(self, place_call_info: str) -> Message:
|
||||
"""Starts an outgoing call."""
|
||||
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
@@ -83,10 +83,11 @@ class Client:
|
||||
|
||||
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():
|
||||
self.account.set_config(key, value)
|
||||
params = {"addr": email, "password": password}
|
||||
self.account.add_or_update_transport(params)
|
||||
self.account.configure()
|
||||
self.logger.debug("Account configured")
|
||||
|
||||
def run_forever(self) -> None:
|
||||
|
||||
@@ -9,7 +9,6 @@ class ContactFlag(IntEnum):
|
||||
"""Bit flags for get_contacts() method."""
|
||||
|
||||
ADD_SELF = 0x02
|
||||
ADDRESS = 0x04
|
||||
|
||||
|
||||
class ChatlistFlag(IntEnum):
|
||||
@@ -73,10 +72,6 @@ class EventType(str, Enum):
|
||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||
ACCOUNTS_CHANGED = "AccountsChanged"
|
||||
ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged"
|
||||
INCOMING_CALL = "IncomingCall"
|
||||
INCOMING_CALL_ACCEPTED = "IncomingCallAccepted"
|
||||
OUTGOING_CALL_ACCEPTED = "OutgoingCallAccepted"
|
||||
CALL_ENDED = "CallEnded"
|
||||
CONFIG_SYNCED = "ConfigSynced"
|
||||
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
||||
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
|
||||
@@ -95,40 +90,10 @@ class ChatType(IntEnum):
|
||||
"""Chat type."""
|
||||
|
||||
UNDEFINED = 0
|
||||
|
||||
SINGLE = 100
|
||||
"""1:1 chat, i.e. a direct chat with a single contact"""
|
||||
|
||||
GROUP = 120
|
||||
|
||||
MAILINGLIST = 140
|
||||
|
||||
OUT_BROADCAST = 160
|
||||
"""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 = 165
|
||||
"""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.
|
||||
"""
|
||||
BROADCAST = 160
|
||||
|
||||
|
||||
class ChatVisibility(str, Enum):
|
||||
@@ -160,6 +125,7 @@ class ViewType(str, Enum):
|
||||
VOICE = "Voice"
|
||||
VIDEO = "Video"
|
||||
FILE = "File"
|
||||
VIDEOCHAT_INVITATION = "VideochatInvitation"
|
||||
WEBXDC = "Webxdc"
|
||||
VCARD = "Vcard"
|
||||
|
||||
@@ -278,3 +244,11 @@ class SocketSecurity(IntEnum):
|
||||
SSL = 1
|
||||
STARTTLS = 2
|
||||
PLAIN = 3
|
||||
|
||||
|
||||
class VideochatType(IntEnum):
|
||||
"""Video chat URL type."""
|
||||
|
||||
UNKNOWN = 0
|
||||
BASICWEBRTC = 1
|
||||
JITSI = 2
|
||||
|
||||
@@ -37,6 +37,10 @@ class Contact:
|
||||
"""Delete contact."""
|
||||
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:
|
||||
"""Change the name of this contact."""
|
||||
self._rpc.change_contact_name(self.account.id, self.id, name)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import json
|
||||
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 .const import EventType
|
||||
@@ -39,11 +39,6 @@ class Message:
|
||||
snapshot["message"] = self
|
||||
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_reactions(self) -> Optional[AttrDict]:
|
||||
"""Get message reactions."""
|
||||
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
||||
@@ -102,15 +97,3 @@ class Message:
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
@@ -13,12 +13,6 @@ from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Messag
|
||||
from ._utils import futuremethod
|
||||
from .rpc import Rpc
|
||||
|
||||
E2EE_INFO_MSGS = 1
|
||||
"""
|
||||
The number of info messages added to new e2ee chats.
|
||||
Currently this is "End-to-end encryption available".
|
||||
"""
|
||||
|
||||
|
||||
class ACFactory:
|
||||
"""Test account factory."""
|
||||
@@ -28,7 +22,9 @@ class ACFactory:
|
||||
|
||||
def get_unconfigured_account(self) -> Account:
|
||||
"""Create a new unconfigured account."""
|
||||
return self.deltachat.add_account()
|
||||
account = self.deltachat.add_account()
|
||||
account.set_config("verified_one_on_one_chats", "1")
|
||||
return account
|
||||
|
||||
def get_unconfigured_bot(self) -> Bot:
|
||||
"""Create a new unconfigured bot."""
|
||||
@@ -73,11 +69,11 @@ class ACFactory:
|
||||
def resetup_account(self, ac: Account) -> Account:
|
||||
"""Resetup account from scratch, losing the encryption key."""
|
||||
ac.stop_io()
|
||||
transports = ac.list_transports()
|
||||
ac.remove()
|
||||
ac_clone = self.get_unconfigured_account()
|
||||
for transport in transports:
|
||||
ac_clone.add_or_update_transport(transport)
|
||||
for i in ["addr", "mail_pw"]:
|
||||
ac_clone.set_config(i, ac.get_config(i))
|
||||
ac.remove()
|
||||
ac_clone.configure()
|
||||
return ac_clone
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||
|
||||
@@ -1,109 +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)
|
||||
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 not incoming_call_event.has_video # Cannot be parsed as SDP, so false by default
|
||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||
assert incoming_call_message.get_call_info().state.kind == "Alerting"
|
||||
assert not 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=-`.
|
||||
place_call_info = """v=0\r
|
||||
o=alice 2890844526 2890844526 IN IP6 2001:db8::3\r
|
||||
s=-\r
|
||||
c=IN IP6 2001:db8::3\r
|
||||
t=0 0\r
|
||||
a=group:BUNDLE foo bar\r
|
||||
\r
|
||||
m=audio 10000 RTP/AVP 0 8 97\r
|
||||
b=AS:200\r
|
||||
a=mid:foo\r
|
||||
a=rtcp-mux\r
|
||||
a=rtpmap:0 PCMU/8000\r
|
||||
a=rtpmap:8 PCMA/8000\r
|
||||
a=rtpmap:97 iLBC/8000\r
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
|
||||
\r
|
||||
m=video 10002 RTP/AVP 31 32\r
|
||||
b=AS:1000\r
|
||||
a=mid:bar\r
|
||||
a=rtcp-mux\r
|
||||
a=rtpmap:31 H261/90000\r
|
||||
a=rtpmap:32 MPV/90000\r
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
|
||||
"""
|
||||
|
||||
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(place_call_info)
|
||||
|
||||
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().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")
|
||||
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)
|
||||
assert msg.get_snapshot().text == "Hello!"
|
||||
break
|
||||
@@ -18,7 +18,9 @@ def test_autocrypt_setup_message_key_transfer(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.add_or_update_transport({"addr": alice1.get_config("addr"), "password": alice1.get_config("mail_pw")})
|
||||
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()
|
||||
@@ -35,7 +37,9 @@ def test_ac_setup_message_twice(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.add_or_update_transport({"addr": alice1.get_config("addr"), "password": alice1.get_config("mail_pw")})
|
||||
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.
|
||||
|
||||
@@ -36,9 +36,6 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
|
||||
# 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)
|
||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -15,14 +16,14 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# 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()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# 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()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
@@ -58,7 +59,8 @@ def test_qr_setup_contact_svg(acfactory) -> None:
|
||||
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)
|
||||
|
||||
# Setup second device for Alice
|
||||
@@ -66,7 +68,8 @@ def test_qr_securejoin(acfactory):
|
||||
alice2 = alice.clone()
|
||||
|
||||
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")
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
@@ -81,15 +84,16 @@ def test_qr_securejoin(acfactory):
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# 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()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
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.chat.get_basic_snapshot().is_protected == protect
|
||||
|
||||
# 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()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
@@ -97,7 +101,7 @@ def test_qr_securejoin(acfactory):
|
||||
# Alice observes securejoin protocol and verifies Bob on second device.
|
||||
alice2.start_io()
|
||||
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()
|
||||
assert alice2_contact_bob_snapshot.is_verified
|
||||
|
||||
@@ -122,8 +126,8 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||
bob_chat_alice = snapshot.chat
|
||||
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
||||
|
||||
alice_chat = alice.create_group("Group")
|
||||
logging.info("Bob joins the group")
|
||||
alice_chat = alice.create_group("Verified group", protect=True)
|
||||
logging.info("Bob joins verified group")
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
while True:
|
||||
@@ -147,8 +151,8 @@ def test_qr_readreceipt(acfactory) -> None:
|
||||
for joiner in [bob, charlie]:
|
||||
joiner.wait_for_securejoin_joiner_success()
|
||||
|
||||
logging.info("Alice creates a group")
|
||||
group = alice.create_group("Group")
|
||||
logging.info("Alice creates a verified group")
|
||||
group = alice.create_group("Group", protect=True)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
|
||||
@@ -209,20 +213,21 @@ def test_setup_contact_resetup(acfactory) -> None:
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
"""Tests verified group recovery by reverifying then removing and adding a member back."""
|
||||
def test_verified_group_recovery(acfactory) -> None:
|
||||
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("ac1 creates a group")
|
||||
chat = ac1.create_group("Group")
|
||||
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 the group")
|
||||
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.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
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
@@ -230,8 +235,6 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
ac3.wait_for_securejoin_joiner_success()
|
||||
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")
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
|
||||
@@ -244,10 +247,85 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
assert len(ac3_chat.get_contacts()) == 3
|
||||
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!
|
||||
|
||||
ac3_contact_ac2 = ac3.create_contact(ac2)
|
||||
ac3_chat.remove_contact(ac3_contact_ac2_old)
|
||||
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
|
||||
ac3_chat.remove_contact(ac3_contact_ac2)
|
||||
|
||||
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
|
||||
@@ -276,16 +354,19 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||
ac1_contact_ac3 = ac1.create_contact(ac3)
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
||||
assert ac1_contact_ac2_snapshot.is_verified
|
||||
assert ac1_contact_ac2_snapshot.verifier_id == ac1_contact_ac3.id
|
||||
assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).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):
|
||||
"""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)
|
||||
|
||||
@@ -298,8 +379,8 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
# we first create a fully joined verified group, and then start
|
||||
# joining a second time but interrupt it, to create pending bob state
|
||||
|
||||
logging.info("ac1: create a group that ac2 fully joins")
|
||||
ch1 = ac1.create_group("Group")
|
||||
logging.info("ac1: create verified group that ac2 fully joins")
|
||||
ch1 = ac1.create_group("Group", protect=True)
|
||||
qr_code = ch1.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
@@ -309,6 +390,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
while 1:
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
if snapshot.text == "ac1 says hello":
|
||||
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||
break
|
||||
|
||||
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
|
||||
@@ -318,18 +400,19 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
|
||||
|
||||
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
|
||||
assert ac3.create_contact(ac2).get_snapshot().is_verified
|
||||
assert ac2.create_contact(ac3).get_snapshot().is_verified
|
||||
assert ac3.get_contact_by_addr(ac2.get_config("addr")).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")
|
||||
vg = ac3.create_group("ac3-created")
|
||||
vg.add_contact(ac3.create_contact(ac2))
|
||||
vg = ac3.create_group("ac3-created", protect=True)
|
||||
vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr")))
|
||||
|
||||
# ensure ac2 receives message in VG
|
||||
vg.send_text("hello")
|
||||
while 1:
|
||||
msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
if msg.text == "hello":
|
||||
assert msg.chat.get_basic_snapshot().is_protected
|
||||
break
|
||||
|
||||
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
|
||||
@@ -353,14 +436,14 @@ def test_qr_new_group_unblocked(acfactory):
|
||||
"""
|
||||
|
||||
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()
|
||||
ac2.secure_join(qr_code)
|
||||
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
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.
|
||||
ac2.wait_for_incoming_msg_event()
|
||||
|
||||
@@ -378,7 +461,8 @@ def test_aeap_flow_verified(acfactory):
|
||||
addr, password = acfactory.get_credentials()
|
||||
|
||||
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()
|
||||
logging.info("ac2: start QR-code based join-group protocol")
|
||||
ac2.secure_join(qr_code)
|
||||
@@ -432,6 +516,7 @@ def test_gossip_verification(acfactory) -> None:
|
||||
|
||||
logging.info("Bob creates an 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_carol)
|
||||
bob_group_chat.send_message(text="Hello Autocrypt group")
|
||||
@@ -440,12 +525,13 @@ def test_gossip_verification(acfactory) -> None:
|
||||
assert snapshot.text == "Hello Autocrypt group"
|
||||
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()
|
||||
assert carol_contact_alice_snapshot.is_verified
|
||||
assert not carol_contact_alice_snapshot.is_verified
|
||||
|
||||
logging.info("Bob creates a Securejoin group")
|
||||
bob_group_chat = bob.create_group("Securejoin Group")
|
||||
bob_group_chat = 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_carol)
|
||||
bob_group_chat.send_message(text="Hello Securejoin group")
|
||||
@@ -468,7 +554,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
# ac3 creates protected group with ac1.
|
||||
ac3_chat = ac3.create_group("Group")
|
||||
ac3_chat = ac3.create_group("Verified group", protect=True)
|
||||
|
||||
# ac1 joins ac3 group.
|
||||
ac3_qr_code = ac3_chat.get_qr_code()
|
||||
@@ -491,8 +577,30 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
|
||||
# ac1 resetups the account.
|
||||
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.remove()
|
||||
@@ -516,6 +624,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.is_info
|
||||
ac2_chat = snapshot.chat
|
||||
assert ac2_chat.get_basic_snapshot().is_protected
|
||||
assert len(ac2_chat.get_contacts()) == 3
|
||||
|
||||
# ac1 is still "not verified" for ac2 due to inconsistent state.
|
||||
@@ -525,8 +634,9 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
def test_withdraw_securejoin_qr(acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
logging.info("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group")
|
||||
logging.info("Alice creates a verified group")
|
||||
alice_chat = alice.create_group("Verified group", protect=True)
|
||||
assert alice_chat.get_basic_snapshot().is_protected
|
||||
logging.info("Bob joins verified group")
|
||||
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
@@ -537,6 +647,7 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
|
||||
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.chat.get_basic_snapshot().is_protected
|
||||
bob_chat.leave()
|
||||
|
||||
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()
|
||||
|
||||
@@ -11,8 +11,7 @@ from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||
from deltachat_rpc_client.const import ChatType, DownloadState, MessageState
|
||||
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
|
||||
from deltachat_rpc_client.const import DownloadState, MessageState
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -171,10 +170,7 @@ def test_account(acfactory) -> None:
|
||||
assert alice.get_size()
|
||||
assert alice.is_configured()
|
||||
assert not alice.get_avatar()
|
||||
# get_contact_by_addr() can lookup a key contact by address:
|
||||
bob_contact = alice.get_contact_by_addr(bob_addr).get_snapshot()
|
||||
assert bob_contact.display_name == "Bob"
|
||||
assert bob_contact.is_key_contact
|
||||
assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
|
||||
assert alice.get_contacts()
|
||||
assert alice.get_contacts(snapshot=True)
|
||||
assert alice.self_contact
|
||||
@@ -252,7 +248,6 @@ def test_chat(acfactory) -> None:
|
||||
bob_chat_alice.get_encryption_info()
|
||||
|
||||
group = alice.create_group("test group")
|
||||
to_resend = group.send_text("will be resent")
|
||||
group.add_contact(alice_contact_bob)
|
||||
group.get_qr_code()
|
||||
|
||||
@@ -264,7 +259,6 @@ def test_chat(acfactory) -> None:
|
||||
|
||||
msg = group.send_message(text="hi")
|
||||
assert (msg.get_snapshot()).text == "hi"
|
||||
group.resend_messages([to_resend])
|
||||
group.forward_messages([msg])
|
||||
|
||||
group.set_draft(text="test draft")
|
||||
@@ -293,6 +287,7 @@ def test_contact(acfactory) -> None:
|
||||
assert repr(alice_contact_bob)
|
||||
alice_contact_bob.block()
|
||||
alice_contact_bob.unblock()
|
||||
alice_contact_bob.reset_encryption()
|
||||
alice_contact_bob.set_name("new name")
|
||||
alice_contact_bob.get_encryption_info()
|
||||
snapshot = alice_contact_bob.get_snapshot()
|
||||
@@ -331,52 +326,6 @@ def test_message(acfactory) -> None:
|
||||
assert reactions == snapshot.reactions
|
||||
|
||||
|
||||
def test_receive_imf_failure(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
|
||||
bob.set_config("fail_on_receiving_full_msg", "1")
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||
assert snapshot.error is not None
|
||||
assert snapshot.show_padlock
|
||||
|
||||
# The failed message doesn't break the IMAP loop.
|
||||
bob.set_config("fail_on_receiving_full_msg", "0")
|
||||
alice_chat_bob.send_text("Hello again!")
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
assert event.chat_id == chat_id
|
||||
msg_id = event.msg_id
|
||||
message1 = bob.get_message_by_id(msg_id)
|
||||
snapshot = message1.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.download_state == DownloadState.DONE
|
||||
assert snapshot.error is None
|
||||
|
||||
# The failed message can be re-downloaded later.
|
||||
bob._rpc.download_full_message(bob.id, message.id)
|
||||
event = bob.wait_for_event(EventType.MSGS_CHANGED)
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.download_state == DownloadState.IN_PROGRESS
|
||||
event = bob.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert event.chat_id == chat_id
|
||||
msg_id = event.msg_id
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.download_state == DownloadState.DONE
|
||||
assert snapshot.error is None
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
|
||||
def test_selfavatar_sync(acfactory, data, log) -> None:
|
||||
alice = acfactory.get_online_account()
|
||||
|
||||
@@ -509,12 +458,8 @@ def test_wait_next_messages(acfactory) -> None:
|
||||
alice_chat_bot.send_text("Hello!")
|
||||
|
||||
next_messages = next_messages_task.result()
|
||||
|
||||
if len(next_messages) == E2EE_INFO_MSGS:
|
||||
next_messages += bot.wait_next_messages()
|
||||
|
||||
assert len(next_messages) == 1 + E2EE_INFO_MSGS
|
||||
snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot()
|
||||
assert len(next_messages) == 1
|
||||
snapshot = next_messages[0].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
|
||||
@@ -570,11 +515,8 @@ def test_provider_info(rpc) -> None:
|
||||
assert provider_info is None
|
||||
|
||||
# Test MX record resolution.
|
||||
# This previously resulted in Gmail provider
|
||||
# because MX record pointed to google.com domain,
|
||||
# but MX record resolution has been removed.
|
||||
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||
assert provider_info is None
|
||||
assert provider_info["id"] == "gmail"
|
||||
|
||||
# Disable MX record resolution.
|
||||
rpc.set_config(account_id, "proxy_enabled", "1")
|
||||
@@ -785,26 +727,6 @@ def test_markseen_contact_request(acfactory):
|
||||
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
|
||||
def test_read_receipt(acfactory):
|
||||
"""
|
||||
Test sending a read receipt and ensure it is attributed to the correct contact.
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
bob.create_chat(alice) # Accept the chat
|
||||
|
||||
alice_chat_bob.send_text("Hello Bob!")
|
||||
msg = bob.wait_for_incoming_msg()
|
||||
msg.mark_seen()
|
||||
|
||||
read_msg = alice.get_message_by_id(alice.wait_for_event(EventType.MSG_READ).msg_id)
|
||||
read_receipts = read_msg.get_read_receipts()
|
||||
assert len(read_receipts) == 1
|
||||
assert read_receipts[0].contact_id == alice_contact_bob.id
|
||||
|
||||
|
||||
def test_get_http_response(acfactory):
|
||||
alice = acfactory.new_configured_account()
|
||||
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
|
||||
@@ -925,36 +847,3 @@ def test_delete_deltachat_folder(acfactory, direct_imap):
|
||||
assert msg.text == "hello"
|
||||
|
||||
assert "DeltaChat" in ac1_direct_imap.list_folders()
|
||||
|
||||
|
||||
def test_broadcast(acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_chat = alice.create_broadcast("My great channel")
|
||||
snapshot = alice_chat.get_basic_snapshot()
|
||||
assert snapshot.name == "My great channel"
|
||||
assert snapshot.is_unpromoted
|
||||
assert snapshot.is_encrypted
|
||||
assert snapshot.chat_type == ChatType.OUT_BROADCAST
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat.add_contact(alice_contact_bob)
|
||||
|
||||
alice_msg = alice_chat.send_message(text="hello").get_snapshot()
|
||||
assert alice_msg.text == "hello"
|
||||
assert alice_msg.show_padlock
|
||||
|
||||
bob_msg = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert bob_msg.text == "hello"
|
||||
assert bob_msg.show_padlock
|
||||
assert bob_msg.error is None
|
||||
|
||||
bob_chat = bob.get_chat_by_id(bob_msg.chat_id)
|
||||
bob_chat_snapshot = bob_chat.get_basic_snapshot()
|
||||
assert bob_chat_snapshot.name == "My great channel"
|
||||
assert not bob_chat_snapshot.is_unpromoted
|
||||
assert bob_chat_snapshot.is_encrypted
|
||||
assert bob_chat_snapshot.chat_type == ChatType.IN_BROADCAST
|
||||
assert bob_chat_snapshot.is_contact_request
|
||||
|
||||
assert not bob_chat.can_send()
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
def test_vcard(acfactory) -> None:
|
||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
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.send_contact(alice_contact_charlie)
|
||||
@@ -15,12 +12,3 @@ def test_vcard(acfactory) -> None:
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.vcard_contact
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.22.0"
|
||||
version = "1.160.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.22.0"
|
||||
"version": "1.160.0"
|
||||
}
|
||||
|
||||
@@ -41,22 +41,22 @@ async fn main_impl() -> Result<()> {
|
||||
if let Some(first_arg) = args.next() {
|
||||
if first_arg.to_str() == Some("--version") {
|
||||
if let Some(arg) = args.next() {
|
||||
return Err(anyhow!("Unrecognized argument {arg:?}"));
|
||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||
}
|
||||
eprintln!("{}", &*DC_VERSION_STR);
|
||||
return Ok(());
|
||||
} else if first_arg.to_str() == Some("--openrpc") {
|
||||
if let Some(arg) = args.next() {
|
||||
return Err(anyhow!("Unrecognized argument {arg:?}"));
|
||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||
}
|
||||
println!("{}", CommandApi::openrpc_specification()?);
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(anyhow!("Unrecognized option {first_arg:?}"));
|
||||
return Err(anyhow!("Unrecognized option {:?}", first_arg));
|
||||
}
|
||||
}
|
||||
if let Some(arg) = args.next() {
|
||||
return Err(anyhow!("Unrecognized argument {arg:?}"));
|
||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||
}
|
||||
|
||||
// Install signal handlers early so that the shutdown is graceful starting from here.
|
||||
|
||||
@@ -20,11 +20,6 @@ impl SystemTimeTools {
|
||||
pub fn shift(duration: Duration) {
|
||||
*SYSTEM_TIME_SHIFT.write().unwrap() += duration;
|
||||
}
|
||||
|
||||
/// Simulates the system clock being rewound by `duration`.
|
||||
pub fn shift_back(duration: Duration) {
|
||||
*SYSTEM_TIME_SHIFT.write().unwrap() -= duration;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
20
deny.toml
20
deny.toml
@@ -7,9 +7,6 @@ ignore = [
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
|
||||
"RUSTSEC-2023-0071",
|
||||
|
||||
# Archived repository
|
||||
"RUSTSEC-2023-0089",
|
||||
|
||||
# Unmaintained instant
|
||||
"RUSTSEC-2024-0384",
|
||||
|
||||
@@ -28,25 +25,31 @@ skip = [
|
||||
{ name = "derive_more-impl", version = "1.0.0" },
|
||||
{ name = "derive_more", version = "1.0.0" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "generator", version = "0.7.5" },
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "hashbrown", version = "0.14.5" },
|
||||
{ name = "heck", version = "0.4.1" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "linux-raw-sys", version = "0.4.14" },
|
||||
{ name = "netdev", version = "0.36.0" },
|
||||
{ name = "netlink-packet-route", version = "0.22.0" },
|
||||
{ name = "loom", version = "0.5.6" },
|
||||
{ name = "lru", version = "0.12.3" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "nom", version = "7.1.3" },
|
||||
{ name = "rand_chacha", version = "0.3.1" },
|
||||
{ name = "rand_core", version = "0.6.4" },
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
{ name = "redox_syscall", version = "0.3.5" },
|
||||
{ name = "redox_syscall", version = "0.4.1" },
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "rustix", version = "0.38.44" },
|
||||
{ name = "rustls-webpki", version = "0.102.8" },
|
||||
{ name = "serdect", version = "0.2.0" },
|
||||
{ name = "socket2", version = "0.5.9" },
|
||||
{ name = "spin", version = "0.9.8" },
|
||||
{ name = "strum_macros", version = "0.26.2" },
|
||||
{ name = "strum", version = "0.26.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "thiserror-impl", version = "1.0.69" },
|
||||
{ name = "thiserror", version = "1.0.69" },
|
||||
{ name = "toml_datetime", version = "0.6.11" },
|
||||
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
|
||||
{ name = "windows" },
|
||||
{ name = "windows_aarch64_gnullvm" },
|
||||
@@ -57,7 +60,6 @@ skip = [
|
||||
{ name = "windows_i686_msvc" },
|
||||
{ name = "windows-implement" },
|
||||
{ name = "windows-interface" },
|
||||
{ name = "windows-link" },
|
||||
{ name = "windows-result" },
|
||||
{ name = "windows-strings" },
|
||||
{ name = "windows-sys" },
|
||||
|
||||
13
flake.nix
13
flake.nix
@@ -98,6 +98,9 @@
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
buildInputs = pkgs.lib.optionals isDarwin [
|
||||
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
};
|
||||
@@ -237,9 +240,6 @@
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
|
||||
CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
@@ -483,6 +483,12 @@
|
||||
pkgs.rustPlatform.cargoSetupHook
|
||||
pkgs.cargo
|
||||
];
|
||||
buildInputs = pkgs.lib.optionals isDarwin [
|
||||
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
|
||||
pkgs.darwin.apple_sdk.frameworks.Security
|
||||
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
|
||||
pkgs.libiconv
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
substituteInPlace $out/include/deltachat.h \
|
||||
@@ -581,7 +587,6 @@
|
||||
(python3.withPackages (pypkgs: with pypkgs; [
|
||||
tox
|
||||
]))
|
||||
nodejs
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
bolero = "0.13.4"
|
||||
bolero = "0.13.3"
|
||||
|
||||
[dependencies]
|
||||
mailparse = { workspace = true }
|
||||
|
||||
49
python/examples/group_tracking.py
Normal file
49
python/examples/group_tracking.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# content of group_tracking.py
|
||||
|
||||
from deltachat import account_hookimpl, run_cmdline
|
||||
|
||||
|
||||
class GroupTrackingPlugin:
|
||||
@account_hookimpl
|
||||
def ac_incoming_message(self, message):
|
||||
print("process_incoming message", message)
|
||||
if message.text.strip() == "/quit":
|
||||
message.account.shutdown()
|
||||
else:
|
||||
# unconditionally accept the chat
|
||||
message.create_chat()
|
||||
addr = message.get_sender_contact().addr
|
||||
text = message.text
|
||||
message.chat.send_text(f"echoing from {addr}:\n{text}")
|
||||
|
||||
@account_hookimpl
|
||||
def ac_outgoing_message(self, message):
|
||||
print("ac_outgoing_message:", message)
|
||||
|
||||
@account_hookimpl
|
||||
def ac_configure_completed(self, success):
|
||||
print("ac_configure_completed:", success)
|
||||
|
||||
@account_hookimpl
|
||||
def ac_chat_modified(self, chat):
|
||||
print("ac_chat_modified:", chat.id, chat.get_name())
|
||||
for member in chat.get_contacts():
|
||||
print(f"chat member: {member.addr}")
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, actor, message):
|
||||
print(f"ac_member_added {contact.addr} to chat {chat.id} from {actor or message.get_sender_contact().addr}")
|
||||
for member in chat.get_contacts():
|
||||
print(f"chat member: {member.addr}")
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, actor, message):
|
||||
print(f"ac_member_removed {contact.addr} from chat {chat.id} by {actor or message.get_sender_contact().addr}")
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
run_cmdline(argv=argv, account_plugins=[GroupTrackingPlugin()])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,7 +1,10 @@
|
||||
import echo_and_quit
|
||||
import group_tracking
|
||||
import py
|
||||
import pytest
|
||||
|
||||
from deltachat.events import FFIEventLogger
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def datadir():
|
||||
@@ -33,3 +36,55 @@ def test_echo_quit_plugin(acfactory, lp):
|
||||
lp.sec("send quit sequence")
|
||||
bot_chat.send_text("/quit")
|
||||
botproc.wait()
|
||||
|
||||
|
||||
def test_group_tracking_plugin(acfactory, lp):
|
||||
lp.sec("creating one group-tracking bot and two temp accounts")
|
||||
botproc = acfactory.run_bot_process(group_tracking)
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
ac1.add_account_plugin(FFIEventLogger(ac1))
|
||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||
|
||||
lp.sec("creating bot test group with bot")
|
||||
bot_chat = ac1.qr_setup_contact(botproc.qr)
|
||||
ac1._evtracker.wait_securejoin_joiner_progress(1000)
|
||||
bot_contact = bot_chat.get_contacts()[0]
|
||||
ch = ac1.create_group_chat("bot test group")
|
||||
ch.add_contact(bot_contact)
|
||||
ch.send_text("hello")
|
||||
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
*ac_chat_modified*bot test group*
|
||||
""",
|
||||
)
|
||||
|
||||
lp.sec("adding third member {}".format(ac2.get_config("addr")))
|
||||
contact3 = ac1.create_contact(ac2)
|
||||
ch.add_contact(contact3)
|
||||
|
||||
reply = ac1._evtracker.wait_next_incoming_message()
|
||||
assert "hello" in reply.text
|
||||
|
||||
lp.sec("now looking at what the bot received")
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
*ac_member_added {}*from*{}*
|
||||
""".format(
|
||||
contact3.addr,
|
||||
ac1.get_config("addr"),
|
||||
),
|
||||
)
|
||||
|
||||
lp.sec("contact successfully added, now removing")
|
||||
ch.remove_contact(contact3)
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
*ac_member_removed {}*from*{}*
|
||||
""".format(
|
||||
contact3.addr,
|
||||
ac1.get_config("addr"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.22.0"
|
||||
version = "1.160.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
@@ -293,8 +293,6 @@ class Account:
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_contact(self, obj) -> Optional[Contact]:
|
||||
if isinstance(obj, Account):
|
||||
return self.create_contact(obj)
|
||||
if isinstance(obj, Contact):
|
||||
return obj
|
||||
(_, addr) = self.get_contact_addr_and_name(obj)
|
||||
@@ -330,21 +328,7 @@ class Account:
|
||||
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
||||
|
||||
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
|
||||
"""Looks up a known and unblocked contact with a given e-mail address.
|
||||
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()."""
|
||||
"""get a contact for the email address or None if it's blocked or doesn't exist."""
|
||||
_, addr = parseaddr(email)
|
||||
addr = as_dc_charpointer(addr)
|
||||
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
||||
@@ -404,16 +388,18 @@ class Account:
|
||||
self,
|
||||
name: str,
|
||||
contacts: Optional[List[Contact]] = None,
|
||||
verified: bool = False,
|
||||
) -> Chat:
|
||||
"""create a new group chat object.
|
||||
|
||||
Chats are unpromoted until the first message is sent.
|
||||
|
||||
:param contacts: list of contacts to add
|
||||
:param verified: if true only verified contacts can be added.
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
"""
|
||||
bytes_name = name.encode("utf8")
|
||||
chat_id = lib.dc_create_group_chat(self._dc_context, 0, bytes_name)
|
||||
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
|
||||
chat = Chat(self, chat_id)
|
||||
if contacts is not None:
|
||||
for contact in contacts:
|
||||
|
||||
@@ -142,6 +142,13 @@ class Chat:
|
||||
"""
|
||||
return bool(lib.dc_chat_can_send(self._dc_chat))
|
||||
|
||||
def is_protected(self) -> bool:
|
||||
"""return True if this chat is a protected chat.
|
||||
|
||||
:returns: True if chat is protected, False otherwise.
|
||||
"""
|
||||
return bool(lib.dc_chat_is_protected(self._dc_chat))
|
||||
|
||||
def get_name(self) -> Optional[str]:
|
||||
"""return name of this chat.
|
||||
|
||||
@@ -410,13 +417,7 @@ class Chat:
|
||||
:raises ValueError: if contact could not be added
|
||||
:returns: None
|
||||
"""
|
||||
from .contact import Contact
|
||||
|
||||
if isinstance(obj, Contact):
|
||||
contact = obj
|
||||
else:
|
||||
contact = self.account.create_contact(obj)
|
||||
|
||||
contact = self.account.create_contact(obj)
|
||||
ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id)
|
||||
if ret != 1:
|
||||
raise ValueError(f"could not add contact {contact!r} to chat")
|
||||
|
||||
@@ -13,6 +13,7 @@ from .account import Account
|
||||
from .capi import ffi, lib
|
||||
from .cutil import from_optional_dc_charpointer
|
||||
from .hookspec import account_hookimpl
|
||||
from .message import map_system_message
|
||||
|
||||
|
||||
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||
@@ -303,15 +304,21 @@ class EventThread(threading.Thread):
|
||||
elif name == "DC_EVENT_INCOMING_MSG":
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
if msg is not None:
|
||||
yield ("ac_incoming_message", {"message": msg})
|
||||
yield map_system_message(msg) or ("ac_incoming_message", {"message": msg})
|
||||
elif name == "DC_EVENT_MSGS_CHANGED":
|
||||
if ffi_event.data2 != 0:
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
if msg is not None:
|
||||
if msg.is_outgoing():
|
||||
res = map_system_message(msg)
|
||||
if res and res[0].startswith("ac_member"):
|
||||
yield res
|
||||
yield "ac_outgoing_message", {"message": msg}
|
||||
elif msg.is_in_fresh():
|
||||
yield "ac_incoming_message", {"message": msg}
|
||||
yield map_system_message(msg) or (
|
||||
"ac_incoming_message",
|
||||
{"message": msg},
|
||||
)
|
||||
elif name == "DC_EVENT_REACTIONS_CHANGED":
|
||||
assert ffi_event.data1 > 0
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Union
|
||||
|
||||
from . import const, props
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer
|
||||
from .reactions import Reactions
|
||||
|
||||
|
||||
class Message:
|
||||
@@ -163,6 +165,17 @@ class Message:
|
||||
),
|
||||
)
|
||||
|
||||
def send_reaction(self, reaction: str):
|
||||
"""Send a reaction to message and return the resulting Message instance."""
|
||||
msg_id = lib.dc_send_reaction(self.account._dc_context, self.id, as_dc_charpointer(reaction))
|
||||
if msg_id == 0:
|
||||
raise ValueError("reaction could not be send")
|
||||
return Message.from_db(self.account, msg_id)
|
||||
|
||||
def get_reactions(self) -> Reactions:
|
||||
"""Get :class:`deltachat.reactions.Reactions` to the message."""
|
||||
return Reactions.from_msg(self)
|
||||
|
||||
def is_system_message(self):
|
||||
"""return True if this message is a system/info message."""
|
||||
return bool(lib.dc_msg_is_info(self._dc_msg))
|
||||
@@ -435,6 +448,10 @@ class Message:
|
||||
"""return True if it's a video message."""
|
||||
return self._view_type == const.DC_MSG_VIDEO
|
||||
|
||||
def is_videochat_invitation(self):
|
||||
"""return True if it's a videochat invitation message."""
|
||||
return self._view_type == const.DC_MSG_VIDEOCHAT_INVITATION
|
||||
|
||||
def is_webxdc(self):
|
||||
"""return True if it's a Webxdc message."""
|
||||
return self._view_type == const.DC_MSG_WEBXDC
|
||||
@@ -475,6 +492,7 @@ _view_type_mapping = {
|
||||
"video": const.DC_MSG_VIDEO,
|
||||
"file": const.DC_MSG_FILE,
|
||||
"sticker": const.DC_MSG_STICKER,
|
||||
"videochat": const.DC_MSG_VIDEOCHAT_INVITATION,
|
||||
"webxdc": const.DC_MSG_WEBXDC,
|
||||
}
|
||||
|
||||
@@ -486,3 +504,56 @@ def get_viewtype_code_from_name(view_type_name):
|
||||
raise ValueError(
|
||||
f"message typecode not found for {view_type_name!r}, available {list(_view_type_mapping.keys())!r}",
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# some helper code for turning system messages into hook events
|
||||
#
|
||||
|
||||
|
||||
def map_system_message(msg):
|
||||
if msg.is_system_message():
|
||||
res = parse_system_add_remove(msg.text)
|
||||
if not res:
|
||||
return None
|
||||
action, affected, actor = res
|
||||
affected = msg.account.get_contact_by_addr(affected)
|
||||
actor = None if actor == "me" else msg.account.get_contact_by_addr(actor)
|
||||
d = {"chat": msg.chat, "contact": affected, "actor": actor, "message": msg}
|
||||
return "ac_member_" + res[0], d
|
||||
|
||||
|
||||
def extract_addr(text):
|
||||
m = re.match(r".*\((.+@.+)\)", text)
|
||||
if m:
|
||||
text = m.group(1)
|
||||
text = text.rstrip(".")
|
||||
return text.strip()
|
||||
|
||||
|
||||
def parse_system_add_remove(text):
|
||||
"""return add/remove info from parsing the given system message text.
|
||||
|
||||
returns a (action, affected, actor) triple
|
||||
"""
|
||||
# You removed member a@b.
|
||||
# You added member a@b.
|
||||
# Member Me (x@y) removed by a@b.
|
||||
# Member x@y added by a@b
|
||||
# Member With space (tmp1@x.org) removed by tmp2@x.org.
|
||||
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
# Group left by some one (tmp1@x.org).
|
||||
# Group left by tmp1@x.org.
|
||||
text = text.lower()
|
||||
m = re.match(r"member (.+) (removed|added) by (.+)", text)
|
||||
if m:
|
||||
affected, action, actor = m.groups()
|
||||
return action, extract_addr(affected), extract_addr(actor)
|
||||
m = re.match(r"you (removed|added) member (.+)", text)
|
||||
if m:
|
||||
action, affected = m.groups()
|
||||
return action, extract_addr(affected), "me"
|
||||
if text.startswith("group left by "):
|
||||
addr = extract_addr(text[13:])
|
||||
if addr:
|
||||
return "removed", addr, addr
|
||||
|
||||
43
python/src/deltachat/reactions.py
Normal file
43
python/src/deltachat/reactions.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""The Reactions object."""
|
||||
|
||||
from .capi import ffi, lib
|
||||
from .cutil import from_dc_charpointer, iter_array
|
||||
|
||||
|
||||
class Reactions:
|
||||
"""Reactions object.
|
||||
|
||||
You obtain instances of it through :class:`deltachat.message.Message`.
|
||||
"""
|
||||
|
||||
def __init__(self, account, dc_reactions) -> None:
|
||||
assert isinstance(account._dc_context, ffi.CData)
|
||||
assert isinstance(dc_reactions, ffi.CData)
|
||||
assert dc_reactions != ffi.NULL
|
||||
self.account = account
|
||||
self._dc_reactions = dc_reactions
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Reactions dc_reactions={self._dc_reactions}>"
|
||||
|
||||
@classmethod
|
||||
def from_msg(cls, msg):
|
||||
assert msg.id > 0
|
||||
return cls(
|
||||
msg.account,
|
||||
ffi.gc(lib.dc_get_msg_reactions(msg.account._dc_context, msg.id), lib.dc_reactions_unref),
|
||||
)
|
||||
|
||||
def get_contacts(self) -> list:
|
||||
"""Get list of contacts reacted to the message.
|
||||
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects for this reaction.
|
||||
"""
|
||||
from .contact import Contact
|
||||
|
||||
dc_array = ffi.gc(lib.dc_reactions_get_contacts(self._dc_reactions), lib.dc_array_unref)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self.account, x)))
|
||||
|
||||
def get_by_contact(self, contact) -> str:
|
||||
"""Get a string containing space-separated reactions of a single :class:`deltachat.contact.Contact`."""
|
||||
return from_dc_charpointer(lib.dc_reactions_get_by_contact_id(self._dc_reactions, contact.id))
|
||||
@@ -20,12 +20,6 @@ import deltachat
|
||||
from . import Account, account_hookimpl, const, get_core_info
|
||||
from .events import FFIEventLogger, FFIEventTracker
|
||||
|
||||
E2EE_INFO_MSGS = 1
|
||||
"""
|
||||
The number of info messages added to new e2ee chats.
|
||||
Currently this is "End-to-end encryption available".
|
||||
"""
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("deltachat testplugin options")
|
||||
@@ -523,6 +517,7 @@ class ACFactory:
|
||||
assert "addr" in configdict and "mail_pw" in configdict, configdict
|
||||
configdict.setdefault("bcc_self", False)
|
||||
configdict.setdefault("mvbox_move", False)
|
||||
configdict.setdefault("sentbox_watch", False)
|
||||
configdict.setdefault("sync_msgs", False)
|
||||
configdict.setdefault("delete_server_after", 0)
|
||||
ac.update_config(configdict)
|
||||
@@ -603,6 +598,20 @@ class ACFactory:
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
def get_protected_chat(self, ac1: Account, ac2: Account):
|
||||
chat = ac1.create_group_chat("Protected Group", verified=True)
|
||||
qr = chat.get_join_qr()
|
||||
ac2.qr_join_chat(qr)
|
||||
ac2._evtracker.wait_securejoin_joiner_progress(1000)
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg is not None
|
||||
assert msg.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg is not None
|
||||
assert "Member Me " in msg.text and " added by " in msg.text
|
||||
return chat
|
||||
|
||||
def introduce_each_other(self, accounts, sending=True):
|
||||
to_wait = []
|
||||
for i, acc in enumerate(accounts):
|
||||
|
||||
@@ -118,7 +118,8 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
ac1_addr = ac1.get_self_contact().addr
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat1 = ac1.create_group_chat("hello")
|
||||
chat1 = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat1.is_protected()
|
||||
qr = chat1.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
@@ -132,7 +133,8 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
assert "added" in msg.text.lower()
|
||||
|
||||
assert any(
|
||||
m.is_system_message() and m.text == "Messages are end-to-end encrypted." for m in msg.chat.get_messages()
|
||||
m.is_system_message() and m.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
for m in msg.chat.get_messages()
|
||||
)
|
||||
lp.sec("ac1: send message")
|
||||
msg_out = chat1.send_text("hello")
|
||||
@@ -141,6 +143,7 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
lp.sec("ac2: read message and check that it's a verified chat")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
assert msg.chat.is_protected()
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac2: Check that ac2 verified ac1")
|
||||
@@ -171,10 +174,8 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
lp.sec("ac2: Check that ac1 verified ac3 for ac2")
|
||||
ac2_ac1_contact = ac2.get_contacts()[0]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
|
||||
for ac2_contact in chat2.get_contacts():
|
||||
if ac2_contact == ac2_ac1_contact or ac2_contact.id == dc.const.DC_CONTACT_ID_SELF:
|
||||
continue
|
||||
assert ac2.get_self_contact().get_verifier(ac2_contact).addr == ac1_addr
|
||||
ac2_ac3_contact = ac2.get_contacts()[1]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
|
||||
|
||||
lp.sec("ac2: send message and let ac3 read it")
|
||||
chat2.send_text("hi")
|
||||
@@ -186,6 +187,83 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
assert msg.is_encrypted()
|
||||
|
||||
|
||||
def test_undecipherable_group(acfactory, lp):
|
||||
"""Test how group messages that cannot be decrypted are
|
||||
handled.
|
||||
|
||||
Group name is encrypted and plaintext subject is set to "..." in
|
||||
this case, so we should assign the messages to existing chat
|
||||
instead of creating a new one. Since there is no existing group
|
||||
chat, the messages should be assigned to 1-1 chat with the sender
|
||||
of the message.
|
||||
"""
|
||||
|
||||
lp.sec("creating and configuring three accounts")
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
acfactory.introduce_each_other([ac1, ac2, ac3])
|
||||
|
||||
lp.sec("ac3 reinstalls DC and generates a new key")
|
||||
ac3.stop_io()
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac4 = acfactory.new_online_configuring_account(cloned_from=ac3)
|
||||
acfactory.wait_configured(ac4)
|
||||
# Create contacts to make sure incoming messages are not treated as contact requests
|
||||
chat41 = ac4.create_chat(ac1)
|
||||
chat42 = ac4.create_chat(ac2)
|
||||
ac4.start_io()
|
||||
ac4._evtracker.wait_idle_inbox_ready()
|
||||
|
||||
lp.sec("ac1: creating group chat with 2 other members")
|
||||
chat = ac1.create_group_chat("title", contacts=[ac2, ac3])
|
||||
|
||||
lp.sec("ac1: send message to new group chat")
|
||||
msg = chat.send_text("hello")
|
||||
|
||||
lp.sec("ac2: checking that the chat arrived correctly")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
|
||||
# ac4 cannot decrypt the message.
|
||||
# Error message should be assigned to the chat with ac1.
|
||||
lp.sec("ac4: checking that message is assigned to the sender chat")
|
||||
error_msg = ac4._evtracker.wait_next_incoming_message()
|
||||
assert error_msg.error # There is an error decrypting the message
|
||||
assert error_msg.chat == chat41
|
||||
|
||||
lp.sec("ac2: sending a reply to the chat")
|
||||
msg.chat.send_text("reply")
|
||||
reply = ac1._evtracker.wait_next_incoming_message()
|
||||
assert reply.text == "reply"
|
||||
assert reply.is_encrypted(), "Reply is not encrypted"
|
||||
|
||||
lp.sec("ac4: checking that reply is assigned to ac2 chat")
|
||||
error_reply = ac4._evtracker.wait_next_incoming_message()
|
||||
assert error_reply.error # There is an error decrypting the message
|
||||
assert error_reply.chat == chat42
|
||||
|
||||
# Test that ac4 replies to error messages don't appear in the
|
||||
# group chat on ac1 and ac2.
|
||||
lp.sec("ac4: replying to ac1 and ac2")
|
||||
|
||||
# Otherwise reply becomes a contact request.
|
||||
chat41.send_text("I can't decrypt your message, ac1!")
|
||||
chat42.send_text("I can't decrypt your message, ac2!")
|
||||
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.error is None
|
||||
assert msg.text == "I can't decrypt your message, ac1!"
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
assert msg.chat == ac1.create_chat(ac3)
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.error is None
|
||||
assert msg.text == "I can't decrypt your message, ac2!"
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
assert msg.chat == ac2.create_chat(ac4)
|
||||
|
||||
|
||||
def test_ephemeral_timer(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -266,7 +344,8 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
ac1_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group_chat("hello")
|
||||
chat = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat.is_protected()
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
@@ -320,7 +399,8 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
ac1.set_avatar(avatar_path)
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group_chat("hello")
|
||||
chat = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat.is_protected()
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
ac2.qr_join_chat(qr)
|
||||
@@ -334,7 +414,8 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
assert msg_in.is_system_message()
|
||||
assert contact.addr == ac1.get_config("addr")
|
||||
chat2 = msg_in.chat
|
||||
assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted."
|
||||
assert chat2.is_protected()
|
||||
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
|
||||
|
||||
lp.sec("ac2_offl: sending message")
|
||||
@@ -373,7 +454,8 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
ac2_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat1 = ac1.create_group_chat("hello")
|
||||
chat1 = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat1.is_protected()
|
||||
qr = chat1.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
@@ -398,17 +480,28 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
|
||||
assert not ac2_offl_ac1_contact.is_verified()
|
||||
chat2_offl = msg_in.chat
|
||||
assert not chat2_offl.is_protected()
|
||||
|
||||
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
|
||||
chat2.send_text("hi2")
|
||||
|
||||
lp.sec("ac2_offl: receiving message")
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert msg_in.is_system_message()
|
||||
assert msg_in.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
|
||||
# We need to consume one event that has data2=0
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
assert ev.data2 == 0
|
||||
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert not msg_in.is_system_message()
|
||||
assert msg_in.text == "hi2"
|
||||
assert msg_in.chat == chat2_offl
|
||||
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
|
||||
assert msg_in.chat.is_protected()
|
||||
assert ac2_offl_ac1_contact.is_verified()
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from imap_tools import AND, U
|
||||
import deltachat as dc
|
||||
from deltachat import account_hookimpl, Message
|
||||
from deltachat.tracker import ImexTracker
|
||||
from deltachat.testplugin import E2EE_INFO_MSGS
|
||||
|
||||
|
||||
def test_basic_imap_api(acfactory, tmp_path):
|
||||
@@ -160,6 +159,32 @@ def test_html_message(acfactory, lp):
|
||||
assert html_text in msg2.html
|
||||
|
||||
|
||||
def test_videochat_invitation_message(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
text = "You are invited to a video chat, click https://meet.jit.si/WxEGad0gGzX to join."
|
||||
|
||||
lp.sec("ac1: prepare and send text message to ac2")
|
||||
msg1 = chat.send_text("message0")
|
||||
assert not msg1.is_videochat_invitation()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message0"
|
||||
assert not msg2.is_videochat_invitation()
|
||||
|
||||
lp.sec("ac1: prepare and send videochat invitation to ac2")
|
||||
msg1 = Message.new_empty(ac1, "videochat")
|
||||
msg1.set_text(text)
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.is_videochat_invitation()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == text
|
||||
assert msg2.is_videochat_invitation()
|
||||
|
||||
|
||||
def test_webxdc_message(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -269,28 +294,26 @@ def test_enable_mvbox_move(acfactory, lp):
|
||||
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
|
||||
|
||||
|
||||
def test_mvbox_thread_and_trash(acfactory, lp):
|
||||
def test_mvbox_sentbox_threads(acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=False)
|
||||
|
||||
lp.sec("ac2: start without a mvbox thread")
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=False)
|
||||
lp.sec("ac2: start without mvbox/sentbox threads")
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=False, sentbox_watch=False)
|
||||
|
||||
lp.sec("ac2 and ac1: waiting for configuration")
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("ac1: create trash")
|
||||
ac1.direct_imap.create_folder("Trash")
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
ac1.stop_io()
|
||||
ac1.start_io()
|
||||
lp.sec("ac1: create and configure sentbox")
|
||||
ac1.direct_imap.create_folder("Sent")
|
||||
ac1.set_config("sentbox_watch", "1")
|
||||
|
||||
lp.sec("ac1: send message and wait for ac2 to receive it")
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
|
||||
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
|
||||
|
||||
assert ac1.get_config("configured_mvbox_folder") == "DeltaChat"
|
||||
while ac1.get_config("configured_trash_folder") != "Trash":
|
||||
while ac1.get_config("configured_sentbox_folder") != "Sent":
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
|
||||
@@ -310,7 +333,7 @@ def test_move_works(acfactory):
|
||||
|
||||
|
||||
def test_move_avoids_loop(acfactory):
|
||||
"""Test that the message is only moved from INBOX to DeltaChat.
|
||||
"""Test that the message is only moved once.
|
||||
|
||||
This is to avoid busy loop if moved message reappears in the Inbox
|
||||
or some scanned folder later.
|
||||
@@ -321,14 +344,6 @@ def test_move_avoids_loop(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
# Create INBOX.DeltaChat folder and make sure
|
||||
# it is detected by full folder scan.
|
||||
ac2.direct_imap.create_folder("INBOX.DeltaChat")
|
||||
ac2.stop_io()
|
||||
ac2.start_io()
|
||||
ac2._evtracker.get_info_contains("Found folders:") # Wait until the end of folder scan.
|
||||
|
||||
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac1_chat.send_text("Message 1")
|
||||
|
||||
@@ -336,27 +351,19 @@ def test_move_avoids_loop(acfactory):
|
||||
ac2_msg1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_msg1.text == "Message 1"
|
||||
|
||||
# Move the message to the INBOX.DeltaChat again.
|
||||
# We assume that test server uses "." as the delimiter.
|
||||
# Move the message to the INBOX again.
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
ac2.direct_imap.conn.move(["*"], "INBOX.DeltaChat")
|
||||
ac2.direct_imap.conn.move(["*"], "INBOX")
|
||||
|
||||
ac1_chat.send_text("Message 2")
|
||||
ac2_msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_msg2.text == "Message 2"
|
||||
|
||||
# Stop and start I/O to trigger folder scan.
|
||||
ac2.stop_io()
|
||||
ac2.start_io()
|
||||
ac2._evtracker.get_info_contains("Found folders:") # Wait until the end of folder scan.
|
||||
|
||||
# Check that Message 1 is still in the INBOX.DeltaChat folder
|
||||
# Check that Message 1 is still in the INBOX folder
|
||||
# and Message 2 is in the DeltaChat folder.
|
||||
ac2.direct_imap.select_folder("INBOX")
|
||||
assert len(ac2.direct_imap.get_all_messages()) == 0
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
assert len(ac2.direct_imap.get_all_messages()) == 1
|
||||
ac2.direct_imap.select_folder("INBOX.DeltaChat")
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
assert len(ac2.direct_imap.get_all_messages()) == 1
|
||||
|
||||
|
||||
@@ -401,10 +408,6 @@ def test_forward_messages(acfactory, lp):
|
||||
msg_out = chat.send_text("message2")
|
||||
|
||||
lp.sec("ac2: wait for receive")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2.get_message_by_id(ev.data2)
|
||||
assert msg_in.text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
assert ev.data2 == msg_out.id
|
||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||
@@ -424,7 +427,7 @@ def test_forward_messages(acfactory, lp):
|
||||
lp.sec("ac2: check new chat has a forwarded message")
|
||||
assert chat3.is_promoted()
|
||||
messages = chat3.get_messages()
|
||||
assert len(messages) == 3
|
||||
assert len(messages) == 2
|
||||
msg = messages[-1]
|
||||
assert msg.is_forwarded()
|
||||
ac2.delete_messages(messages)
|
||||
@@ -468,19 +471,14 @@ def test_resend_message(acfactory, lp):
|
||||
lp.sec("ac2: receive message")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == "message"
|
||||
chat2 = msg_in.chat
|
||||
chat2_msg_cnt = len(chat2.get_messages())
|
||||
|
||||
lp.sec("ac1: resend message")
|
||||
ac1.resend_messages([msg_in])
|
||||
|
||||
lp.sec("ac1: send another message")
|
||||
chat1.send_text("another message")
|
||||
|
||||
lp.sec("ac2: receive another message")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == "another message"
|
||||
chat2 = msg_in.chat
|
||||
chat2_msg_cnt = len(chat2.get_messages())
|
||||
|
||||
lp.sec("ac2: check that message is deleted")
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
assert len(chat2.get_messages()) == chat2_msg_cnt
|
||||
|
||||
|
||||
@@ -624,11 +622,6 @@ def test_moved_markseen(acfactory):
|
||||
|
||||
with ac2.direct_imap.idle() as idle2:
|
||||
ac2.start_io()
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg.text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
|
||||
@@ -745,7 +738,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
lp.sec("sending text message from ac1 to ac2")
|
||||
msg_out = chat.send_text("message1")
|
||||
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
assert len(chat.get_messages()) == 1
|
||||
|
||||
lp.sec("disable ac1 MDNs")
|
||||
ac1.set_config("mdns_enabled", "0")
|
||||
@@ -753,7 +746,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
assert len(msg.chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
|
||||
lp.sec("ac2: mark incoming message as seen")
|
||||
ac2.mark_seen_messages([msg])
|
||||
@@ -762,7 +755,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
# MDN should be moved even though MDNs are already disabled
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
assert len(chat.get_messages()) == 1
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
|
||||
@@ -774,7 +767,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_send_receive_encrypt(acfactory, lp):
|
||||
def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
ac1.get_device_chat().mark_noticed()
|
||||
@@ -805,11 +798,12 @@ def test_send_receive_encrypt(acfactory, lp):
|
||||
msg3.mark_seen()
|
||||
assert not list(ac1.get_fresh_messages())
|
||||
|
||||
lp.sec("create group chat with two members")
|
||||
lp.sec("create group chat with two members, one of which has no encrypt state")
|
||||
chat = ac1.create_group_chat("encryption test")
|
||||
chat.add_contact(ac2)
|
||||
chat.add_contact(ac1.create_contact("notexisting@testrun.org"))
|
||||
msg = chat.send_text("test not encrypt")
|
||||
assert msg.is_encrypted()
|
||||
assert not msg.is_encrypted()
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
|
||||
|
||||
@@ -855,9 +849,9 @@ def test_no_draft_if_cant_send(acfactory):
|
||||
|
||||
def test_dont_show_emails(acfactory, lp):
|
||||
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
|
||||
So: If it's outgoing AND there is no Received header, then ignore the email.
|
||||
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
|
||||
|
||||
If the draft email is sent out and received later (i.e. it's in "Inbox"), it must be shown.
|
||||
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown.
|
||||
|
||||
Also, test that unknown emails in the Spam folder are not shown."""
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
@@ -866,6 +860,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
|
||||
acfactory.wait_configured(ac1)
|
||||
ac1.direct_imap.create_folder("Drafts")
|
||||
ac1.direct_imap.create_folder("Sent")
|
||||
ac1.direct_imap.create_folder("Spam")
|
||||
ac1.direct_imap.create_folder("Junk")
|
||||
|
||||
@@ -881,7 +876,21 @@ def test_dont_show_emails(acfactory, lp):
|
||||
Message-ID: <aepiors@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
message in Drafts received later
|
||||
message in Drafts that is moved to Sent later
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Sent",
|
||||
"""
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.org
|
||||
Message-ID: <hsabaeni@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
message in Sent
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
@@ -961,13 +970,14 @@ def test_dont_show_emails(acfactory, lp):
|
||||
lp.sec("All prepared, now let DC find the message")
|
||||
ac1.start_io()
|
||||
|
||||
msg = ac1._evtracker.wait_next_messages_changed()
|
||||
|
||||
# Wait until each folder was scanned, this is necessary for this test to test what it should test:
|
||||
ac1._evtracker.wait_idle_inbox_ready()
|
||||
|
||||
fresh_msgs = list(ac1.get_fresh_messages())
|
||||
msg = fresh_msgs[0]
|
||||
assert msg.text == "subj – message in Sent"
|
||||
chat_msgs = msg.chat.get_messages()
|
||||
assert len(chat_msgs) == 1
|
||||
assert len(chat_msgs) == 2
|
||||
assert any(msg.text == "subj – Actually interesting message in Spam" for msg in chat_msgs)
|
||||
|
||||
assert not any("unknown.address" in c.get_name() for c in ac1.get_chats())
|
||||
@@ -975,16 +985,16 @@ def test_dont_show_emails(acfactory, lp):
|
||||
assert ac1.direct_imap.get_uid_by_message_id("spam.message@junk.org")
|
||||
|
||||
ac1.stop_io()
|
||||
lp.sec("'Send out' the draft by moving it to Inbox, and wait for DC to display it this time")
|
||||
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time")
|
||||
ac1.direct_imap.select_folder("Drafts")
|
||||
uid = ac1.direct_imap.get_uid_by_message_id("aepiors@example.org")
|
||||
ac1.direct_imap.conn.move(uid, "Inbox")
|
||||
ac1.direct_imap.conn.move(uid, "Sent")
|
||||
|
||||
ac1.start_io()
|
||||
msg2 = ac1._evtracker.wait_next_messages_changed()
|
||||
|
||||
assert msg2.text == "subj – message in Drafts received later"
|
||||
assert len(msg.chat.get_messages()) == 2
|
||||
assert msg2.text == "subj – message in Drafts that is moved to Sent later"
|
||||
assert len(msg.chat.get_messages()) == 3
|
||||
|
||||
|
||||
def test_bot(acfactory, lp):
|
||||
@@ -1114,11 +1124,6 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
assert m == msg_out
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
|
||||
msg_in = ac2.get_message_by_id(ev.data2)
|
||||
assert msg_in.text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 == msg_out.id
|
||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||
@@ -1134,9 +1139,9 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
|
||||
lp.sec("create some chat content")
|
||||
some1_addr = some1.get_config("addr")
|
||||
chat1 = ac1.create_contact(some1).create_chat()
|
||||
chat1 = ac1.create_contact(some1_addr, name="some1").create_chat()
|
||||
chat1.send_text("msg1")
|
||||
assert len(ac1.get_contacts()) == 1
|
||||
assert len(ac1.get_contacts(query="some1")) == 1
|
||||
|
||||
original_image_path = data.get_path("d.png")
|
||||
chat1.send_image(original_image_path)
|
||||
@@ -1148,16 +1153,16 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
chat1.send_file(str(path))
|
||||
|
||||
def assert_account_is_proper(ac):
|
||||
contacts = ac.get_contacts()
|
||||
contacts = ac.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == some1_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 3 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert messages[1 + E2EE_INFO_MSGS].filemime == "image/png"
|
||||
assert os.stat(messages[1 + E2EE_INFO_MSGS].filename).st_size == os.stat(original_image_path).st_size
|
||||
assert len(messages) == 3
|
||||
assert messages[0].text == "msg1"
|
||||
assert messages[1].filemime == "image/png"
|
||||
assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size
|
||||
ac.set_config("displayname", "new displayname")
|
||||
assert ac.get_config("displayname") == "new displayname"
|
||||
|
||||
@@ -1211,7 +1216,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
|
||||
def test_qr_email_capitalization(acfactory, lp):
|
||||
"""Regression test for a bug
|
||||
that resulted in failure to propagate verification
|
||||
that resulted in failure to propagate verification via gossip in a verified group
|
||||
when the database already contained the contact with a different email address capitalization.
|
||||
"""
|
||||
|
||||
@@ -1222,17 +1227,17 @@ def test_qr_email_capitalization(acfactory, lp):
|
||||
lp.sec(f"ac1 creates a contact for ac2 ({ac2_addr_uppercase})")
|
||||
ac1.create_contact(ac2_addr_uppercase)
|
||||
|
||||
lp.sec("ac3 creates a group with a QR code")
|
||||
chat = ac3.create_group_chat("hello")
|
||||
lp.sec("ac3 creates a verified group with a QR code")
|
||||
chat = ac3.create_group_chat("hello", verified=True)
|
||||
qr = chat.get_join_qr()
|
||||
|
||||
lp.sec("ac1 joins a group via a QR code")
|
||||
lp.sec("ac1 joins a verified group via a QR code")
|
||||
ac1_chat = ac1.qr_join_chat(qr)
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "Member Me added by {}.".format(ac3.get_config("addr"))
|
||||
assert len(ac1_chat.get_contacts()) == 2
|
||||
|
||||
lp.sec("ac2 joins a group via a QR code")
|
||||
lp.sec("ac2 joins a verified group via a QR code")
|
||||
ac2.qr_join_chat(qr)
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
@@ -1283,6 +1288,79 @@ def test_set_get_contact_avatar(acfactory, data, lp):
|
||||
assert msg6.get_sender_contact().get_profile_image() is None
|
||||
|
||||
|
||||
def test_add_remove_member_remote_events(acfactory, lp):
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
ac1_addr = ac1.get_config("addr")
|
||||
ac3_addr = ac3.get_config("addr")
|
||||
# activate local plugin for ac2
|
||||
in_list = queue.Queue()
|
||||
|
||||
class EventHolder:
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
class InPlugin:
|
||||
@account_hookimpl
|
||||
def ac_incoming_message(self, message):
|
||||
# we immediately accept the sender because
|
||||
# otherwise we won't see member_added contacts
|
||||
message.create_chat()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_chat_modified(self, chat):
|
||||
in_list.put(EventHolder(action="chat-modified", chat=chat))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, message):
|
||||
in_list.put(EventHolder(action="added", chat=chat, contact=contact, message=message))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, message):
|
||||
in_list.put(EventHolder(action="removed", chat=chat, contact=contact, message=message))
|
||||
|
||||
ac2.add_account_plugin(InPlugin())
|
||||
|
||||
lp.sec("ac1: create group chat with ac2")
|
||||
chat = ac1.create_group_chat("hello", contacts=[ac2])
|
||||
|
||||
lp.sec("ac1: send a message to group chat to promote the group")
|
||||
chat.send_text("afterwards promoted")
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
assert chat.is_promoted()
|
||||
assert sorted(x.addr for x in chat.get_contacts()) == sorted(x.addr for x in ev.chat.get_contacts())
|
||||
|
||||
lp.sec("ac1: add address2")
|
||||
# note that if the above create_chat() would not
|
||||
# happen we would not receive a proper member_added event
|
||||
contact2 = chat.add_contact(ac3)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get()
|
||||
assert ev.action == "added"
|
||||
assert ev.message.get_sender_contact().addr == ac1_addr
|
||||
assert ev.contact.addr == ac3_addr
|
||||
|
||||
lp.sec("ac1: remove address2")
|
||||
chat.remove_contact(contact2)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get()
|
||||
assert ev.action == "removed"
|
||||
assert ev.contact.addr == contact2.addr
|
||||
assert ev.message.get_sender_contact().addr == ac1_addr
|
||||
|
||||
lp.sec("ac1: remove ac2 contact from chat")
|
||||
chat.remove_contact(ac2)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get()
|
||||
assert ev.action == "removed"
|
||||
assert ev.message.get_sender_contact().addr == ac1_addr
|
||||
|
||||
|
||||
def test_system_group_msg_from_blocked_user(acfactory, lp):
|
||||
"""
|
||||
Tests that a blocked user removes you from a group.
|
||||
@@ -1410,8 +1488,8 @@ def test_connectivity(acfactory, lp):
|
||||
ac1.maybe_network()
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 1 + E2EE_INFO_MSGS
|
||||
assert msgs[0 + E2EE_INFO_MSGS].text == "Hi"
|
||||
assert len(msgs) == 1
|
||||
assert msgs[0].text == "Hi"
|
||||
|
||||
lp.sec("Test that the connectivity changes to WORKING while new messages are fetched")
|
||||
|
||||
@@ -1421,8 +1499,8 @@ def test_connectivity(acfactory, lp):
|
||||
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 2 + E2EE_INFO_MSGS
|
||||
assert msgs[1 + E2EE_INFO_MSGS].text == "Hi 2"
|
||||
assert len(msgs) == 2
|
||||
assert msgs[1].text == "Hi 2"
|
||||
|
||||
|
||||
def test_fetch_deleted_msg(acfactory, lp):
|
||||
@@ -1682,6 +1760,44 @@ def test_configure_error_msgs_invalid_server(acfactory):
|
||||
assert "configuration" not in ev.data2.lower()
|
||||
|
||||
|
||||
def test_name_changes(acfactory):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("displayname", "Account 1")
|
||||
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
contact = None
|
||||
|
||||
def update_name():
|
||||
"""Send a message from ac1 to ac2 to update the name"""
|
||||
nonlocal contact
|
||||
chat12.send_text("Hello")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
contact = msg.get_sender_contact()
|
||||
return contact.name
|
||||
|
||||
assert update_name() == "Account 1"
|
||||
|
||||
ac1.set_config("displayname", "Account 1 revision 2")
|
||||
assert update_name() == "Account 1 revision 2"
|
||||
|
||||
# Explicitly rename contact on ac2 to "Renamed"
|
||||
ac2.create_contact(contact, name="Renamed")
|
||||
assert contact.name == "Renamed"
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
|
||||
assert ev.data1 == contact.id
|
||||
|
||||
# ac1 also renames itself into "Renamed"
|
||||
assert update_name() == "Renamed"
|
||||
ac1.set_config("displayname", "Renamed")
|
||||
assert update_name() == "Renamed"
|
||||
|
||||
# Contact name was set to "Renamed" explicitly before,
|
||||
# so it should not be changed.
|
||||
ac1.set_config("displayname", "Renamed again")
|
||||
updated_name = update_name()
|
||||
assert updated_name == "Renamed"
|
||||
|
||||
|
||||
def test_status(acfactory):
|
||||
"""Test that status is transferred over the network."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
@@ -1762,12 +1878,12 @@ def test_group_quote(acfactory, lp):
|
||||
"xyz",
|
||||
False,
|
||||
"xyz",
|
||||
), # Test that emails aren't found in a random folder
|
||||
), # Test that emails are recognized in a random folder but not moved
|
||||
(
|
||||
"xyz",
|
||||
True,
|
||||
"xyz",
|
||||
), # ...emails are found in a random folder and downloaded without moving
|
||||
"DeltaChat",
|
||||
), # ...emails are found in a random folder and moved to DeltaChat
|
||||
(
|
||||
"Spam",
|
||||
False,
|
||||
@@ -1792,7 +1908,7 @@ def test_scan_folders(acfactory, lp, folder, move, expected_destination):
|
||||
ac1.stop_io()
|
||||
assert folder in ac1.direct_imap.list_folders()
|
||||
|
||||
lp.sec("Send a message to from ac2 to ac1 and manually move it to `folder`")
|
||||
lp.sec("Send a message to from ac2 to ac1 and manually move it to the mvbox")
|
||||
ac1.direct_imap.select_config_folder("inbox")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
|
||||
@@ -1802,17 +1918,10 @@ def test_scan_folders(acfactory, lp, folder, move, expected_destination):
|
||||
lp.sec("start_io() and see if DeltaChat finds the message (" + variant + ")")
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
ac1.start_io()
|
||||
chat = ac1.create_chat(ac2)
|
||||
n_msgs = 1 # "Messages are end-to-end encrypted."
|
||||
if folder == "Spam":
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
n_msgs += 1
|
||||
else:
|
||||
ac1._evtracker.wait_idle_inbox_ready()
|
||||
assert len(chat.get_messages()) == n_msgs
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
|
||||
# The message has reached its destination.
|
||||
# The message has been downloaded, which means it has reached its destination.
|
||||
ac1.direct_imap.select_folder(expected_destination)
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 1
|
||||
if folder != expected_destination:
|
||||
|
||||
@@ -1,12 +1,51 @@
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
import deltachat as dc
|
||||
from deltachat.tracker import ImexFailed
|
||||
from deltachat import Account, Message
|
||||
from deltachat.testplugin import E2EE_INFO_MSGS
|
||||
from deltachat import Account, account_hookimpl, Message
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("msgtext", "res"),
|
||||
[
|
||||
(
|
||||
"Member Me (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org"),
|
||||
),
|
||||
(
|
||||
"Member With space (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org"),
|
||||
),
|
||||
(
|
||||
"Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org"),
|
||||
),
|
||||
(
|
||||
"Member With space (tmp1@x.org) removed by me",
|
||||
("removed", "tmp1@x.org", "me"),
|
||||
),
|
||||
(
|
||||
"Group left by some one (tmp1@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org"),
|
||||
),
|
||||
("Group left by tmp1@x.org.", ("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
(
|
||||
"Member tmp1@x.org added by tmp2@x.org.",
|
||||
("added", "tmp1@x.org", "tmp2@x.org"),
|
||||
),
|
||||
("Member nothing bla bla", None),
|
||||
("Another unknown system message", None),
|
||||
],
|
||||
)
|
||||
def test_parse_system_add_remove(msgtext, res):
|
||||
from deltachat.message import parse_system_add_remove
|
||||
|
||||
out = parse_system_add_remove(msgtext)
|
||||
assert out == res
|
||||
|
||||
|
||||
class TestOfflineAccountBasic:
|
||||
@@ -138,15 +177,14 @@ class TestOfflineContact:
|
||||
|
||||
def test_get_contacts_and_delete(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact(ac2)
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contacts = ac1.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contact1 in contacts
|
||||
|
||||
assert not ac1.get_contacts(query="some2")
|
||||
assert not ac1.get_contacts(query="some1")
|
||||
assert ac1.get_contacts(query="some1")
|
||||
assert len(ac1.get_contacts(with_self=True)) == 2
|
||||
assert contact1 in ac1.get_contacts()
|
||||
|
||||
assert ac1.delete_contact(contact1)
|
||||
assert contact1 not in ac1.get_contacts()
|
||||
@@ -161,9 +199,9 @@ class TestOfflineContact:
|
||||
def test_create_chat_flexibility(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat1 = ac1.create_chat(ac2) # This creates a key-contact chat
|
||||
chat2 = ac1.create_chat(ac2.get_self_contact().addr) # This creates address-contact chat
|
||||
assert chat1 != chat2
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
chat2 = ac1.create_chat(ac2.get_self_contact().addr)
|
||||
assert chat1 == chat2
|
||||
ac3 = acfactory.get_unconfigured_account()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.create_chat(ac3)
|
||||
@@ -221,18 +259,17 @@ class TestOfflineChat:
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
with pytest.raises(ValueError):
|
||||
chat.add_contact(ac2.get_self_contact())
|
||||
contact = chat.add_contact(ac2)
|
||||
assert contact.addr == ac2.get_config("addr")
|
||||
assert contact.name == ac2.get_config("displayname")
|
||||
assert contact.account == ac1
|
||||
chat.remove_contact(ac2)
|
||||
|
||||
def test_group_chat_creation(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
ac3 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact(ac2)
|
||||
contact2 = ac1.create_contact(ac3)
|
||||
def test_group_chat_creation(self, ac1):
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some2@example.org", name="some2")
|
||||
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
|
||||
assert chat.get_name() == "title1"
|
||||
assert contact1 in chat.get_contacts()
|
||||
@@ -271,21 +308,21 @@ class TestOfflineChat:
|
||||
chat.set_name("Homework")
|
||||
assert chat.get_messages()[-1].text == "abc homework xyz Homework"
|
||||
|
||||
def test_group_chat_qr(self, acfactory, ac1):
|
||||
@pytest.mark.parametrize("verified", [True, False])
|
||||
def test_group_chat_qr(self, acfactory, ac1, verified):
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
chat = ac1.create_group_chat(name="title1", verified=verified)
|
||||
assert chat.is_group()
|
||||
qr = chat.get_join_qr()
|
||||
assert ac2.check_qr(qr).is_ask_verifygroup
|
||||
|
||||
def test_removing_blocked_user_from_group(self, ac1, acfactory, lp):
|
||||
def test_removing_blocked_user_from_group(self, ac1, lp):
|
||||
"""
|
||||
Test that blocked contact is not unblocked when removed from a group.
|
||||
See https://github.com/deltachat/deltachat-core-rust/issues/2030
|
||||
"""
|
||||
lp.sec("Create a group chat with a contact")
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
contact = ac1.create_contact(ac2)
|
||||
contact = ac1.create_contact("some1@example.org")
|
||||
group = ac1.create_group_chat("title", contacts=[contact])
|
||||
group.send_text("First group message")
|
||||
|
||||
@@ -297,6 +334,10 @@ class TestOfflineChat:
|
||||
group.remove_contact(contact)
|
||||
assert contact.is_blocked()
|
||||
|
||||
lp.sec("ac1 adding blocked contact unblocks it")
|
||||
group.add_contact(contact)
|
||||
assert not contact.is_blocked()
|
||||
|
||||
def test_get_set_profile_image_simple(self, ac1, data):
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
p = data.get_path("d.png")
|
||||
@@ -439,8 +480,7 @@ class TestOfflineChat:
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac_contact = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact(ac_contact).create_chat()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
@@ -455,15 +495,15 @@ class TestOfflineChat:
|
||||
assert os.path.exists(path)
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2.import_all(path)
|
||||
contacts = ac2.get_contacts()
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
|
||||
passphrase1 = "passphrase1"
|
||||
@@ -471,9 +511,8 @@ class TestOfflineChat:
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
ac1 = acfactory.get_pseudo_configured_account(passphrase=passphrase1)
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
|
||||
chat = ac1.create_contact(ac2).create_chat()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
@@ -494,15 +533,15 @@ class TestOfflineChat:
|
||||
ac2.import_all(path)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts()
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
contact2_addr = contact2.addr
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
ac2.shutdown()
|
||||
|
||||
@@ -511,15 +550,15 @@ class TestOfflineChat:
|
||||
ac2.open(passphrase2)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts()
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == contact2_addr
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_export_with_passphrase(self, acfactory, tmp_path):
|
||||
passphrase = "test_passphrase"
|
||||
@@ -527,9 +566,8 @@ class TestOfflineChat:
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac_contact = acfactory.get_pseudo_configured_account()
|
||||
|
||||
chat = ac1.create_contact(ac_contact).create_chat()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
@@ -551,15 +589,15 @@ class TestOfflineChat:
|
||||
ac2.import_all(path, passphrase)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts()
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
|
||||
"""
|
||||
@@ -573,8 +611,7 @@ class TestOfflineChat:
|
||||
backupdir.mkdir()
|
||||
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac_contact = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact(ac_contact).create_chat()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
@@ -597,15 +634,15 @@ class TestOfflineChat:
|
||||
ac2.import_all(path, bak_passphrase)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts()
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
ac2.shutdown()
|
||||
|
||||
@@ -614,15 +651,15 @@ class TestOfflineChat:
|
||||
ac2.open(acct_passphrase)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts()
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_set_get_draft(self, chat1):
|
||||
msg1 = Message.new_empty(chat1.account, "text")
|
||||
@@ -644,10 +681,78 @@ class TestOfflineChat:
|
||||
assert not res.is_ask_verifygroup()
|
||||
assert res.contact_id == 10
|
||||
|
||||
def test_audit_log_view_without_daymarker(self, acfactory, lp):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
def test_group_chat_many_members_add_remove(self, ac1, lp):
|
||||
lp.sec("ac1: creating group chat with 10 other members")
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
# promote chat
|
||||
chat.send_text("hello")
|
||||
assert chat.is_promoted()
|
||||
|
||||
# activate local plugin
|
||||
in_list = []
|
||||
|
||||
class InPlugin:
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, actor):
|
||||
in_list.append(("added", chat, contact, actor))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, actor):
|
||||
in_list.append(("removed", chat, contact, actor))
|
||||
|
||||
ac1.add_account_plugin(InPlugin())
|
||||
|
||||
# perform add contact many times
|
||||
contacts = []
|
||||
for i in range(10):
|
||||
lp.sec("create contact")
|
||||
contact = ac1.create_contact(f"some{i}@example.org")
|
||||
contacts.append(contact)
|
||||
lp.sec("add contact")
|
||||
chat.add_contact(contact)
|
||||
|
||||
assert chat.num_contacts() == 11
|
||||
|
||||
# let's make sure the events perform plugin hooks
|
||||
def wait_events(cond):
|
||||
now = time.time()
|
||||
while time.time() < now + 5:
|
||||
if cond():
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
pytest.fail("failed to get events")
|
||||
|
||||
wait_events(lambda: len(in_list) == 10)
|
||||
|
||||
assert len(in_list) == 10
|
||||
chat_contacts = chat.get_contacts()
|
||||
for in_cmd, in_chat, in_contact, in_actor in in_list:
|
||||
assert in_cmd == "added"
|
||||
assert in_chat == chat
|
||||
assert in_contact in chat_contacts
|
||||
assert in_actor is None
|
||||
chat_contacts.remove(in_contact)
|
||||
|
||||
assert chat_contacts[0].id == 1 # self contact
|
||||
|
||||
in_list[:] = []
|
||||
|
||||
lp.sec("ac1: removing two contacts and checking things are right")
|
||||
chat.remove_contact(contacts[9])
|
||||
chat.remove_contact(contacts[3])
|
||||
assert chat.num_contacts() == 9
|
||||
|
||||
wait_events(lambda: len(in_list) == 2)
|
||||
assert len(in_list) == 2
|
||||
assert in_list[0][0] == "removed"
|
||||
assert in_list[0][1] == chat
|
||||
assert in_list[0][2] == contacts[9]
|
||||
assert in_list[1][0] == "removed"
|
||||
assert in_list[1][1] == chat
|
||||
assert in_list[1][2] == contacts[3]
|
||||
|
||||
def test_audit_log_view_without_daymarker(self, ac1, lp):
|
||||
lp.sec("ac1: test audit log (show only system messages)")
|
||||
chat = ac1.create_group_chat(name="audit log sample data")
|
||||
|
||||
@@ -656,10 +761,10 @@ class TestOfflineChat:
|
||||
assert chat.is_promoted()
|
||||
|
||||
lp.sec("create test data")
|
||||
chat.add_contact(ac2)
|
||||
chat.add_contact(ac1.create_contact("some-1@example.org"))
|
||||
chat.set_name("audit log test group")
|
||||
chat.send_text("a message in between")
|
||||
|
||||
lp.sec("check message count of only system messages (without daymarkers)")
|
||||
sysmessages = [x for x in chat.get_messages() if x.is_system_message()]
|
||||
assert len(sysmessages) == 4
|
||||
assert len(sysmessages) == 3
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-10-17
|
||||
2025-06-22
|
||||
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.90.0
|
||||
RUST_VERSION=1.87.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=d041136c19a48b493823b46d472f12b9ee94ae80
|
||||
REV=77cbf92a8565fdf1bcaba10fa93c1455c750a1e9
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
@@ -15,7 +15,7 @@ cd "$TMP"
|
||||
git checkout "$REV"
|
||||
DATE=$(git show -s --format=%cs)
|
||||
"$CORE_ROOT"/scripts/create-provider-data-rs.py "$TMP/_providers" "$DATE" >"$CORE_ROOT/src/provider/data.rs"
|
||||
rustfmt --edition 2024 "$CORE_ROOT/src/provider/data.rs"
|
||||
rustfmt "$CORE_ROOT/src/provider/data.rs"
|
||||
rm -fr "$TMP"
|
||||
|
||||
cd "$CORE_ROOT"
|
||||
|
||||
20
spec.md
20
spec.md
@@ -1,6 +1,6 @@
|
||||
# Chatmail Specification
|
||||
|
||||
Version: 0.37.0
|
||||
Version: 0.36.0
|
||||
Status: In-progress
|
||||
Format: [Semantic Line Breaks](https://sembr.org/)
|
||||
|
||||
@@ -582,24 +582,6 @@ and e.g. simply search for the line starting with `EMAIL`
|
||||
in order to get the email address.
|
||||
|
||||
|
||||
# Verifications
|
||||
|
||||
Keys obtained using [SecureJoin](https://securejoin.readthedocs.io) protocol
|
||||
and corresponding contacts
|
||||
are considered "verified".
|
||||
|
||||
As an extension to `Autocrypt-Gossip` header,
|
||||
chatmail clients can add `_verified=1` attribute
|
||||
(underscore marks the attribute as non-critical)
|
||||
to indicate that they have the gossiped key
|
||||
and the corresponding contact marked as verified.
|
||||
|
||||
When receiving such `Autocrypt-Gossip` header
|
||||
in a message signed by a verified key,
|
||||
chatmail clients mark the gossiped key
|
||||
as indirectly verified.
|
||||
|
||||
|
||||
# Transitioning to a new e-mail address (AEAP)
|
||||
|
||||
When receiving a message:
|
||||
|
||||
117
src/accounts.rs
117
src/accounts.rs
@@ -1,10 +1,10 @@
|
||||
//! # Account manager module.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::BTreeMap;
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -14,7 +14,7 @@ use uuid::Uuid;
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
use tokio::sync::oneshot;
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
use tokio::time::{Duration, sleep};
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
use crate::context::{Context, ContextBuilder};
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
@@ -53,14 +53,6 @@ impl Accounts {
|
||||
Accounts::open(dir, writable).await
|
||||
}
|
||||
|
||||
/// Get the ID used to log events.
|
||||
///
|
||||
/// Account manager logs events with ID 0
|
||||
/// which is not used by any accounts.
|
||||
fn get_id(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
/// Creates a new default structure.
|
||||
async fn create(dir: &Path) -> Result<()> {
|
||||
fs::create_dir_all(dir)
|
||||
@@ -78,7 +70,7 @@ impl Accounts {
|
||||
ensure!(dir.exists(), "directory does not exist");
|
||||
|
||||
let config_file = dir.join(CONFIG_NAME);
|
||||
ensure!(config_file.exists(), "{config_file:?} does not exist");
|
||||
ensure!(config_file.exists(), "{:?} does not exist", config_file);
|
||||
|
||||
let config = Config::from_file(config_file, writable).await?;
|
||||
let events = Events::new();
|
||||
@@ -270,51 +262,9 @@ impl Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a list of all account ids in the user-configured order.
|
||||
/// Get a list of all account ids.
|
||||
pub fn get_all(&self) -> Vec<u32> {
|
||||
let mut ordered_ids = Vec::new();
|
||||
let mut all_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
|
||||
|
||||
// First, add accounts in the configured order
|
||||
for &id in &self.config.inner.accounts_order {
|
||||
if all_ids.remove(&id) {
|
||||
ordered_ids.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Then add any accounts not in the order list (newly added accounts)
|
||||
for id in all_ids {
|
||||
ordered_ids.push(id);
|
||||
}
|
||||
|
||||
ordered_ids
|
||||
}
|
||||
|
||||
/// Sets the order of accounts.
|
||||
///
|
||||
/// The provided list should contain all account IDs in the desired order.
|
||||
/// If an account ID is missing from the list, it will be appended at the end.
|
||||
/// If the list contains non-existent account IDs, they will be ignored.
|
||||
pub async fn set_accounts_order(&mut self, order: Vec<u32>) -> Result<()> {
|
||||
let existing_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
|
||||
|
||||
// Filter out non-existent account IDs
|
||||
let mut filtered_order: Vec<u32> = order
|
||||
.into_iter()
|
||||
.filter(|id| existing_ids.contains(id))
|
||||
.collect();
|
||||
|
||||
// Add any missing account IDs at the end
|
||||
for &id in &existing_ids {
|
||||
if !filtered_order.contains(&id) {
|
||||
filtered_order.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
self.config.inner.accounts_order = filtered_order;
|
||||
self.config.sync().await?;
|
||||
self.emit_event(EventType::AccountsChanged);
|
||||
Ok(())
|
||||
self.accounts.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
|
||||
@@ -353,11 +303,11 @@ impl Accounts {
|
||||
/// This is an auxiliary function and not part of public API.
|
||||
/// Use [Accounts::background_fetch] instead.
|
||||
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
|
||||
let n_accounts = accounts.len();
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::Info(format!(
|
||||
"Starting background fetch for {n_accounts} accounts."
|
||||
"Starting background fetch for {} accounts.",
|
||||
accounts.len()
|
||||
)),
|
||||
});
|
||||
let mut set = JoinSet::new();
|
||||
@@ -369,12 +319,6 @@ impl Accounts {
|
||||
});
|
||||
}
|
||||
set.join_all().await;
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::Info(format!(
|
||||
"Finished background fetch for {n_accounts} accounts."
|
||||
)),
|
||||
});
|
||||
}
|
||||
|
||||
/// Auxiliary function for [Accounts::background_fetch].
|
||||
@@ -408,10 +352,7 @@ impl Accounts {
|
||||
///
|
||||
/// Returns a future that resolves when background fetch is done,
|
||||
/// but does not capture `&self`.
|
||||
pub fn background_fetch(
|
||||
&self,
|
||||
timeout: std::time::Duration,
|
||||
) -> impl Future<Output = ()> + use<> {
|
||||
pub fn background_fetch(&self, timeout: std::time::Duration) -> impl Future<Output = ()> {
|
||||
let accounts: Vec<Context> = self.accounts.values().cloned().collect();
|
||||
let events = self.events.clone();
|
||||
Self::background_fetch_with_timeout(accounts, events, timeout)
|
||||
@@ -463,10 +404,6 @@ struct InnerConfig {
|
||||
pub selected_account: u32,
|
||||
pub next_id: u32,
|
||||
pub accounts: Vec<AccountConfig>,
|
||||
/// Ordered list of account IDs, representing the user's preferred order.
|
||||
/// If an account ID is not in this list, it will be appended at the end.
|
||||
#[serde(default)]
|
||||
pub accounts_order: Vec<u32>,
|
||||
}
|
||||
|
||||
impl Drop for Config {
|
||||
@@ -519,9 +456,7 @@ impl Config {
|
||||
Ok(())
|
||||
});
|
||||
if locked_rx.await.is_err() {
|
||||
bail!(
|
||||
"Delta Chat is already running. To use Delta Chat, you must first close the existing Delta Chat process, or restart your device. (accounts.lock file is already locked)"
|
||||
);
|
||||
bail!("Delta Chat is already running. To use Delta Chat, you must first close the existing Delta Chat process, or restart your device. (accounts.lock file is already locked)");
|
||||
};
|
||||
Ok(Some(lock_task))
|
||||
}
|
||||
@@ -533,7 +468,6 @@ impl Config {
|
||||
accounts: Vec::new(),
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
accounts_order: Vec::new(),
|
||||
};
|
||||
if !lock {
|
||||
let cfg = Self {
|
||||
@@ -566,13 +500,11 @@ impl Config {
|
||||
/// protects from parallel calls resulting to a wrong file contents.
|
||||
async fn sync(&mut self) -> Result<()> {
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
ensure!(
|
||||
!self
|
||||
.lock_task
|
||||
.as_ref()
|
||||
.context("Config is read-only")?
|
||||
.is_finished()
|
||||
);
|
||||
ensure!(!self
|
||||
.lock_task
|
||||
.as_ref()
|
||||
.context("Config is read-only")?
|
||||
.is_finished());
|
||||
|
||||
let tmp_path = self.file.with_extension("toml.tmp");
|
||||
let mut file = fs::File::create(&tmp_path)
|
||||
@@ -666,10 +598,6 @@ impl Config {
|
||||
uuid,
|
||||
});
|
||||
self.inner.next_id += 1;
|
||||
|
||||
// Add new account to the end of the order list
|
||||
self.inner.accounts_order.push(id);
|
||||
|
||||
id
|
||||
};
|
||||
|
||||
@@ -691,10 +619,6 @@ impl Config {
|
||||
// remove account from the configs
|
||||
self.inner.accounts.remove(idx);
|
||||
}
|
||||
|
||||
// Remove from order list as well
|
||||
self.inner.accounts_order.retain(|&x| x != id);
|
||||
|
||||
if self.inner.selected_account == id {
|
||||
// reset selected account
|
||||
self.inner.selected_account = self
|
||||
@@ -724,7 +648,8 @@ impl Config {
|
||||
{
|
||||
ensure!(
|
||||
self.inner.accounts.iter().any(|e| e.id == id),
|
||||
"invalid account id: {id}"
|
||||
"invalid account id: {}",
|
||||
id
|
||||
);
|
||||
|
||||
self.inner.selected_account = id;
|
||||
@@ -1037,11 +962,9 @@ mod tests {
|
||||
|
||||
// Test that event emitter does not return `None` immediately.
|
||||
let duration = std::time::Duration::from_millis(1);
|
||||
assert!(
|
||||
tokio::time::timeout(duration, event_emitter.recv())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(tokio::time::timeout(duration, event_emitter.recv())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// When account manager is dropped, event emitter is exhausted.
|
||||
drop(accounts);
|
||||
|
||||
109
src/aheader.rs
109
src/aheader.rs
@@ -6,7 +6,7 @@ use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context as _, Error, Result, bail};
|
||||
use anyhow::{bail, Context as _, Error, Result};
|
||||
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
|
||||
@@ -17,6 +17,7 @@ pub enum EncryptPreference {
|
||||
#[default]
|
||||
NoPreference = 0,
|
||||
Mutual = 1,
|
||||
Reset = 20,
|
||||
}
|
||||
|
||||
impl fmt::Display for EncryptPreference {
|
||||
@@ -24,6 +25,7 @@ impl fmt::Display for EncryptPreference {
|
||||
match *self {
|
||||
EncryptPreference::Mutual => write!(fmt, "mutual"),
|
||||
EncryptPreference::NoPreference => write!(fmt, "nopreference"),
|
||||
EncryptPreference::Reset => write!(fmt, "reset"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +37,7 @@ impl FromStr for EncryptPreference {
|
||||
match s {
|
||||
"mutual" => Ok(EncryptPreference::Mutual),
|
||||
"nopreference" => Ok(EncryptPreference::NoPreference),
|
||||
_ => bail!("Cannot parse encryption preference {s}"),
|
||||
_ => bail!("Cannot parse encryption preference {}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,13 +48,21 @@ pub struct Aheader {
|
||||
pub addr: String,
|
||||
pub public_key: SignedPublicKey,
|
||||
pub prefer_encrypt: EncryptPreference,
|
||||
}
|
||||
|
||||
// Whether `_verified` attribute is present.
|
||||
//
|
||||
// `_verified` attribute is an extension to `Autocrypt-Gossip`
|
||||
// header that is used to tell that the sender
|
||||
// marked this key as verified.
|
||||
pub verified: bool,
|
||||
impl Aheader {
|
||||
/// Creates new autocrypt header
|
||||
pub fn new(
|
||||
addr: String,
|
||||
public_key: SignedPublicKey,
|
||||
prefer_encrypt: EncryptPreference,
|
||||
) -> Self {
|
||||
Aheader {
|
||||
addr,
|
||||
public_key,
|
||||
prefer_encrypt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Aheader {
|
||||
@@ -61,9 +71,6 @@ impl fmt::Display for Aheader {
|
||||
if self.prefer_encrypt == EncryptPreference::Mutual {
|
||||
write!(fmt, " prefer-encrypt=mutual;")?;
|
||||
}
|
||||
if self.verified {
|
||||
write!(fmt, " _verified=1;")?;
|
||||
}
|
||||
|
||||
// adds a whitespace every 78 characters, this allows
|
||||
// email crate to wrap the lines according to RFC 5322
|
||||
@@ -118,8 +125,6 @@ impl FromStr for Aheader {
|
||||
.and_then(|raw| raw.parse().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let verified = attributes.remove("_verified").is_some();
|
||||
|
||||
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
|
||||
// Autocrypt-Level0: unknown attribute, treat the header as invalid
|
||||
if attributes.keys().any(|k| !k.starts_with('_')) {
|
||||
@@ -130,7 +135,6 @@ impl FromStr for Aheader {
|
||||
addr,
|
||||
public_key,
|
||||
prefer_encrypt,
|
||||
verified,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -148,11 +152,10 @@ mod tests {
|
||||
|
||||
assert_eq!(h.addr, "me@mail.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
|
||||
assert_eq!(h.verified, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Non-standard values of prefer-encrypt such as `reset` are treated as no preference.
|
||||
// EncryptPreference::Reset is an internal value, parser should never return it
|
||||
#[test]
|
||||
fn test_from_str_reset() -> Result<()> {
|
||||
let raw = format!("addr=reset@example.com; prefer-encrypt=reset; keydata={RAWKEY}");
|
||||
@@ -214,9 +217,7 @@ mod tests {
|
||||
let rendered = ah.to_string();
|
||||
assert_eq!(rendered, fixed_header);
|
||||
|
||||
let ah = Aheader::from_str(&format!(
|
||||
" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {RAWKEY}"
|
||||
))?;
|
||||
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {RAWKEY}"))?;
|
||||
assert_eq!(ah.addr, "a@b.example.org");
|
||||
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
@@ -239,60 +240,38 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_display_aheader() {
|
||||
assert!(
|
||||
format!(
|
||||
"{}",
|
||||
Aheader {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false
|
||||
}
|
||||
assert!(format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"test@example.com".to_string(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::Mutual
|
||||
)
|
||||
.contains("prefer-encrypt=mutual;")
|
||||
);
|
||||
)
|
||||
.contains("prefer-encrypt=mutual;"));
|
||||
|
||||
// According to Autocrypt Level 1 specification,
|
||||
// only "prefer-encrypt=mutual;" can be used.
|
||||
// If the setting is nopreference, the whole attribute is omitted.
|
||||
assert!(
|
||||
!format!(
|
||||
"{}",
|
||||
Aheader {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
verified: false
|
||||
}
|
||||
assert!(!format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"test@example.com".to_string(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::NoPreference
|
||||
)
|
||||
.contains("prefer-encrypt")
|
||||
);
|
||||
)
|
||||
.contains("prefer-encrypt"));
|
||||
|
||||
// Always lowercase the address in the header.
|
||||
assert!(
|
||||
format!(
|
||||
"{}",
|
||||
Aheader {
|
||||
addr: "TeSt@eXaMpLe.cOm".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false
|
||||
}
|
||||
assert!(format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"TeSt@eXaMpLe.cOm".to_string(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::Mutual
|
||||
)
|
||||
.contains("test@example.com")
|
||||
);
|
||||
|
||||
assert!(
|
||||
format!(
|
||||
"{}",
|
||||
Aheader {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
verified: true
|
||||
}
|
||||
)
|
||||
.contains("_verified")
|
||||
);
|
||||
)
|
||||
.contains("test@example.com"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ pub(crate) async fn handle_authres(
|
||||
let from_domain = match EmailAddress::new(from) {
|
||||
Ok(email) => email.domain,
|
||||
Err(e) => {
|
||||
return Err(anyhow::format_err!("invalid email {from}: {e:#}"));
|
||||
return Err(anyhow::format_err!("invalid email {}: {:#}", from, e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -266,6 +266,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::mimeparser;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools;
|
||||
@@ -468,7 +469,7 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
|
||||
// The ordering in which the emails are received can matter;
|
||||
// the test _should_ pass for every ordering.
|
||||
dir.sort_by_key(|d| d.file_name());
|
||||
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::rng());
|
||||
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::thread_rng());
|
||||
|
||||
for entry in &dir {
|
||||
let mut file = fs::File::open(entry.path()).await?;
|
||||
@@ -519,6 +520,41 @@ Authentication-Results: dkim=";
|
||||
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
|
||||
}
|
||||
|
||||
// Test that Autocrypt works with mailing list.
|
||||
//
|
||||
// Previous versions of Delta Chat ignored Autocrypt based on the List-Post header.
|
||||
// This is not needed: comparing of the From address to Autocrypt header address is enough.
|
||||
// If the mailing list is not rewriting the From header, Autocrypt should be applied.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_autocrypt_in_mailinglist_not_ignored() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let alice_bob_chat = alice.create_chat(&bob).await;
|
||||
let bob_alice_chat = bob.create_chat(&alice).await;
|
||||
let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
|
||||
sent.payload
|
||||
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
|
||||
bob.recv_msg(&sent).await;
|
||||
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
|
||||
assert!(peerstate.is_some());
|
||||
|
||||
// Bob can now write encrypted to Alice:
|
||||
let mut sent = bob
|
||||
.send_text(bob_alice_chat.id, "hellooo in the mailinglist again")
|
||||
.await;
|
||||
assert!(sent.load_from_db().await.get_showpadlock());
|
||||
|
||||
sent.payload
|
||||
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
|
||||
let rcvd = alice.recv_msg(&sent).await;
|
||||
assert!(rcvd.get_showpadlock());
|
||||
assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_authres_in_mailinglist_ignored() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -548,13 +584,12 @@ Authentication-Results: dkim=";
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
|
||||
// The message info should contain a warning:
|
||||
assert!(
|
||||
rcvd.id
|
||||
.get_info(&bob)
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("DKIM Results: Passed=false")
|
||||
);
|
||||
assert!(rcvd
|
||||
.id
|
||||
.get_info(&bob)
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("DKIM Results: Passed=false"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
95
src/blob.rs
95
src/blob.rs
@@ -6,11 +6,11 @@ use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context as _, Result, ensure, format_err};
|
||||
use anyhow::{ensure, format_err, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use futures::StreamExt;
|
||||
use image::ImageReader;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::ImageReader;
|
||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::{fs, task};
|
||||
@@ -20,8 +20,7 @@ use crate::config::Config;
|
||||
use crate::constants::{self, MediaQuality};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::message::Viewtype;
|
||||
use crate::log::{error, info, warn, LogExt};
|
||||
use crate::tools::sanitize_filename;
|
||||
|
||||
/// Represents a file in the blob directory.
|
||||
@@ -170,7 +169,7 @@ impl<'a> BlobObject<'a> {
|
||||
false => name,
|
||||
};
|
||||
if !BlobObject::is_acceptible_blob_name(name) {
|
||||
return Err(format_err!("not an acceptable blob name: {name}"));
|
||||
return Err(format_err!("not an acceptable blob name: {}", name));
|
||||
}
|
||||
Ok(BlobObject {
|
||||
blobdir: context.get_blobdir(),
|
||||
@@ -206,7 +205,11 @@ impl<'a> BlobObject<'a> {
|
||||
/// to be lowercase.
|
||||
pub fn suffix(&self) -> Option<&str> {
|
||||
let ext = self.name.rsplit('.').next();
|
||||
if ext == Some(&self.name) { None } else { ext }
|
||||
if ext == Some(&self.name) {
|
||||
None
|
||||
} else {
|
||||
ext
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether a name is a valid blob name.
|
||||
@@ -264,30 +267,32 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
};
|
||||
|
||||
let viewtype = &mut Viewtype::Image;
|
||||
let maybe_sticker = &mut false;
|
||||
let is_avatar = true;
|
||||
self.check_or_recode_to_size(
|
||||
context, None, // The name of an avatar doesn't matter
|
||||
viewtype, img_wh, max_bytes, is_avatar,
|
||||
self.recode_to_size(
|
||||
context,
|
||||
None, // The name of an avatar doesn't matter
|
||||
maybe_sticker,
|
||||
img_wh,
|
||||
max_bytes,
|
||||
is_avatar,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks or recodes an image pointed by the [BlobObject] so that it fits into limits on the
|
||||
/// image width, height and file size specified by the config.
|
||||
/// Recodes an image pointed by a [BlobObject] so that it fits into limits on the image width,
|
||||
/// height and file size specified by the config.
|
||||
///
|
||||
/// Recoding is only done for [`Viewtype::Image`]. For [`Viewtype::File`], if it's a correct
|
||||
/// image, `*viewtype` is set to [`Viewtype::Image`].
|
||||
///
|
||||
/// On some platforms images are passed to Core as [`Viewtype::Sticker`]. We recheck if the
|
||||
/// image is a true sticker assuming that it must have at least one fully transparent corner,
|
||||
/// otherwise `*viewtype` is set to [`Viewtype::Image`].
|
||||
pub async fn check_or_recode_image(
|
||||
/// On some platforms images are passed to the core as [`crate::message::Viewtype::Sticker`] in
|
||||
/// which case `maybe_sticker` flag should be set. We recheck if an image is a true sticker
|
||||
/// assuming that it must have at least one fully transparent corner, otherwise this flag is
|
||||
/// reset.
|
||||
pub async fn recode_to_image_size(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
name: Option<String>,
|
||||
viewtype: &mut Viewtype,
|
||||
maybe_sticker: &mut bool,
|
||||
) -> Result<String> {
|
||||
let (img_wh, max_bytes) =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
@@ -300,10 +305,13 @@ impl<'a> BlobObject<'a> {
|
||||
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
|
||||
};
|
||||
let is_avatar = false;
|
||||
self.check_or_recode_to_size(context, name, viewtype, img_wh, max_bytes, is_avatar)
|
||||
let new_name =
|
||||
self.recode_to_size(context, name, maybe_sticker, img_wh, max_bytes, is_avatar)?;
|
||||
|
||||
Ok(new_name)
|
||||
}
|
||||
|
||||
/// Checks or recodes the image so that it fits into limits on width/height and byte size.
|
||||
/// Recodes the image so that it fits into limits on width/height and byte size.
|
||||
///
|
||||
/// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `img_wh` and proceeds
|
||||
/// with the result without rechecking.
|
||||
@@ -314,11 +322,11 @@ impl<'a> BlobObject<'a> {
|
||||
/// then the updated user-visible filename will be returned;
|
||||
/// this may be necessary because the format may be changed to JPG,
|
||||
/// i.e. "image.png" -> "image.jpg".
|
||||
fn check_or_recode_to_size(
|
||||
fn recode_to_size(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
name: Option<String>,
|
||||
viewtype: &mut Viewtype,
|
||||
maybe_sticker: &mut bool,
|
||||
mut img_wh: u32,
|
||||
max_bytes: usize,
|
||||
is_avatar: bool,
|
||||
@@ -329,7 +337,6 @@ impl<'a> BlobObject<'a> {
|
||||
let no_exif_ref = &mut no_exif;
|
||||
let mut name = name.unwrap_or_else(|| self.name.clone());
|
||||
let original_name = name.clone();
|
||||
let vt = &mut *viewtype;
|
||||
let res: Result<String> = tokio::task::block_in_place(move || {
|
||||
let mut file = std::fs::File::open(self.to_abs_path())?;
|
||||
let (nr_bytes, exif) = image_metadata(&file)?;
|
||||
@@ -348,30 +355,22 @@ impl<'a> BlobObject<'a> {
|
||||
)
|
||||
}
|
||||
};
|
||||
let fmt = imgreader.format().context("Unknown format")?;
|
||||
if *vt == Viewtype::File {
|
||||
*vt = Viewtype::Image;
|
||||
return Ok(name);
|
||||
}
|
||||
let fmt = imgreader.format().context("No format??")?;
|
||||
let mut img = imgreader.decode().context("image decode failure")?;
|
||||
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
|
||||
let mut encoded = Vec::new();
|
||||
|
||||
if *vt == Viewtype::Sticker {
|
||||
if *maybe_sticker {
|
||||
let x_max = img.width().saturating_sub(1);
|
||||
let y_max = img.height().saturating_sub(1);
|
||||
if !img.in_bounds(x_max, y_max)
|
||||
|| !(img.get_pixel(0, 0).0[3] == 0
|
||||
*maybe_sticker = img.in_bounds(x_max, y_max)
|
||||
&& (img.get_pixel(0, 0).0[3] == 0
|
||||
|| img.get_pixel(x_max, 0).0[3] == 0
|
||||
|| img.get_pixel(0, y_max).0[3] == 0
|
||||
|| img.get_pixel(x_max, y_max).0[3] == 0)
|
||||
{
|
||||
*vt = Viewtype::Image;
|
||||
} else {
|
||||
// Core doesn't auto-assign `Viewtype::Sticker` to messages and stickers coming
|
||||
// from UIs shouldn't contain sensitive Exif info.
|
||||
return Ok(name);
|
||||
}
|
||||
|| img.get_pixel(x_max, y_max).0[3] == 0);
|
||||
}
|
||||
if *maybe_sticker && exif.is_none() {
|
||||
return Ok(name);
|
||||
}
|
||||
|
||||
img = match orientation {
|
||||
@@ -458,7 +457,8 @@ impl<'a> BlobObject<'a> {
|
||||
{
|
||||
if img_wh < 20 {
|
||||
return Err(format_err!(
|
||||
"Failed to scale image to below {max_bytes}B.",
|
||||
"Failed to scale image to below {}B.",
|
||||
max_bytes,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -504,11 +504,10 @@ impl<'a> BlobObject<'a> {
|
||||
Ok(_) => res,
|
||||
Err(err) => {
|
||||
if !is_avatar && no_exif {
|
||||
error!(
|
||||
warn!(
|
||||
context,
|
||||
"Cannot check/recode image, using original data: {err:#}.",
|
||||
"Cannot recode image, using original data: {err:#}.",
|
||||
);
|
||||
*viewtype = Viewtype::File;
|
||||
Ok(original_name)
|
||||
} else {
|
||||
Err(err)
|
||||
@@ -537,11 +536,7 @@ fn file_hash(src: &Path) -> Result<blake3::Hash> {
|
||||
fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
|
||||
let len = file.metadata()?.len();
|
||||
let mut bufreader = std::io::BufReader::new(file);
|
||||
let exif = exif::Reader::new()
|
||||
.continue_on_error(true)
|
||||
.read_from_container(&mut bufreader)
|
||||
.or_else(|e| e.distill_partial_result(|_errors| {}))
|
||||
.ok();
|
||||
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
|
||||
Ok((len, exif))
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::*;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::sql;
|
||||
use crate::test_utils::{self, AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext};
|
||||
use crate::test_utils::{self, TestContext};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
|
||||
@@ -140,9 +140,9 @@ async fn test_add_white_bg() {
|
||||
|
||||
let mut blob = BlobObject::create_and_deduplicate(&t, &avatar_src, &avatar_src).unwrap();
|
||||
let img_wh = 128;
|
||||
let viewtype = &mut Viewtype::Image;
|
||||
let maybe_sticker = &mut false;
|
||||
let strict_limits = true;
|
||||
blob.check_or_recode_to_size(&t, None, viewtype, img_wh, 20_000, strict_limits)
|
||||
blob.recode_to_size(&t, None, maybe_sticker, img_wh, 20_000, strict_limits)
|
||||
.unwrap();
|
||||
tokio::task::block_in_place(move || {
|
||||
let img = ImageReader::open(blob.to_abs_path())
|
||||
@@ -188,9 +188,9 @@ async fn test_selfavatar_outside_blobdir() {
|
||||
);
|
||||
|
||||
let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap();
|
||||
let viewtype = &mut Viewtype::Image;
|
||||
let maybe_sticker = &mut false;
|
||||
let strict_limits = true;
|
||||
blob.check_or_recode_to_size(&t, None, viewtype, 1000, 3000, strict_limits)
|
||||
blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits)
|
||||
.unwrap();
|
||||
let new_file_size = file_size(&blob.to_abs_path()).await;
|
||||
assert!(new_file_size <= 3000);
|
||||
@@ -241,8 +241,9 @@ async fn test_selfavatar_in_blobdir() {
|
||||
async fn test_selfavatar_copy_without_recode() {
|
||||
let t = TestContext::new().await;
|
||||
let avatar_src = t.dir.path().join("avatar.png");
|
||||
fs::write(&avatar_src, AVATAR_64x64_BYTES).await.unwrap();
|
||||
let avatar_blob = t.get_blobdir().join(AVATAR_64x64_DEDUPLICATED);
|
||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||
let avatar_blob = t.get_blobdir().join("e9b6c7a78aa2e4f415644f55a553e73.png");
|
||||
assert!(!avatar_blob.exists());
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
@@ -250,7 +251,7 @@ async fn test_selfavatar_copy_without_recode() {
|
||||
assert!(avatar_blob.exists());
|
||||
assert_eq!(
|
||||
fs::metadata(&avatar_blob).await.unwrap().len(),
|
||||
AVATAR_64x64_BYTES.len() as u64
|
||||
avatar_bytes.len() as u64
|
||||
);
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||
@@ -334,28 +335,6 @@ async fn test_recode_image_2() {
|
||||
assert_correct_rotation(&img_rotated);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_bad_exif() {
|
||||
// `exiftool` reports for this file "Bad offset for IFD0 XResolution", still Exif must be
|
||||
// detected and removed.
|
||||
let bytes = include_bytes!("../../test-data/image/1000x1000-bad-exif.jpg");
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 1000,
|
||||
original_height: 1000,
|
||||
compressed_width: 1000,
|
||||
compressed_height: 1000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_balanced_png() {
|
||||
let bytes = include_bytes!("../../test-data/image/screenshot.png");
|
||||
@@ -421,7 +400,7 @@ async fn test_recode_image_balanced_png() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// This will be sent as Image, see [`BlobObject::check_or_recode_image()`] for explanation.
|
||||
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Sticker,
|
||||
media_quality_config: "0",
|
||||
@@ -438,28 +417,6 @@ async fn test_recode_image_balanced_png() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sticker_with_exif() {
|
||||
let bytes = include_bytes!("../../test-data/image/logo-exif.png");
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Sticker,
|
||||
bytes,
|
||||
extension: "png",
|
||||
// TODO: Pretend there's no Exif. Currently `exif` crate doesn't detect Exif in this image,
|
||||
// so the test doesn't check all the logic it should.
|
||||
has_exif: false,
|
||||
original_width: 135,
|
||||
original_height: 135,
|
||||
res_viewtype: Some(Viewtype::Sticker),
|
||||
compressed_width: 135,
|
||||
compressed_height: 135,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Tests that RGBA PNG can be recoded into JPEG
|
||||
/// by dropping alpha channel.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -529,7 +486,6 @@ struct SendImageCheckMediaquality<'a> {
|
||||
pub(crate) original_width: u32,
|
||||
pub(crate) original_height: u32,
|
||||
pub(crate) orientation: i32,
|
||||
pub(crate) res_viewtype: Option<Viewtype>,
|
||||
pub(crate) compressed_width: u32,
|
||||
pub(crate) compressed_height: u32,
|
||||
pub(crate) set_draft: bool,
|
||||
@@ -545,7 +501,6 @@ impl SendImageCheckMediaquality<'_> {
|
||||
let original_width = self.original_width;
|
||||
let original_height = self.original_height;
|
||||
let orientation = self.orientation;
|
||||
let res_viewtype = self.res_viewtype.unwrap_or(Viewtype::Image);
|
||||
let compressed_width = self.compressed_width;
|
||||
let compressed_height = self.compressed_height;
|
||||
let set_draft = self.set_draft;
|
||||
@@ -596,7 +551,7 @@ impl SendImageCheckMediaquality<'_> {
|
||||
}
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_msg.get_viewtype(), res_viewtype);
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = bob
|
||||
@@ -610,7 +565,7 @@ impl SendImageCheckMediaquality<'_> {
|
||||
}
|
||||
|
||||
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||
assert!(res_viewtype != Viewtype::Image || exif.is_none());
|
||||
assert!(exif.is_none());
|
||||
|
||||
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
||||
|
||||
@@ -625,23 +580,20 @@ impl SendImageCheckMediaquality<'_> {
|
||||
}
|
||||
|
||||
fn assert_extension(context: &TestContext, msg: Message, extension: &str) {
|
||||
assert!(
|
||||
msg.param
|
||||
.get(Param::File)
|
||||
.unwrap()
|
||||
.ends_with(&format!(".{extension}"))
|
||||
);
|
||||
assert!(
|
||||
msg.param
|
||||
.get(Param::Filename)
|
||||
.unwrap()
|
||||
.ends_with(&format!(".{extension}"))
|
||||
);
|
||||
assert!(
|
||||
msg.get_filename()
|
||||
.unwrap()
|
||||
.ends_with(&format!(".{extension}"))
|
||||
);
|
||||
assert!(msg
|
||||
.param
|
||||
.get(Param::File)
|
||||
.unwrap()
|
||||
.ends_with(&format!(".{extension}")));
|
||||
assert!(msg
|
||||
.param
|
||||
.get(Param::Filename)
|
||||
.unwrap()
|
||||
.ends_with(&format!(".{extension}")));
|
||||
assert!(msg
|
||||
.get_filename()
|
||||
.unwrap()
|
||||
.ends_with(&format!(".{extension}")));
|
||||
assert_eq!(
|
||||
msg.get_file(context)
|
||||
.unwrap()
|
||||
|
||||
702
src/calls.rs
702
src/calls.rs
@@ -1,702 +0,0 @@
|
||||
//! # Handle calls.
|
||||
//!
|
||||
//! Internally, calls are bound a user-visible message initializing the call.
|
||||
//! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs.
|
||||
use crate::chat::ChatIdBlocked;
|
||||
use crate::chat::{Chat, ChatId, send_msg};
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::log::{info, warn};
|
||||
use crate::message::{self, Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::net::dns::lookup_host_with_cache;
|
||||
use crate::param::Param;
|
||||
use crate::tools::time;
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use sdp::SessionDescription;
|
||||
use serde::Serialize;
|
||||
use std::io::Cursor;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use tokio::task;
|
||||
use tokio::time::sleep;
|
||||
|
||||
/// How long callee's or caller's phone ring.
|
||||
///
|
||||
/// For the callee, this is to prevent endless ringing
|
||||
/// in case the initial "call" is received, but then the caller went offline.
|
||||
/// Moreover, this prevents outdated calls to ring
|
||||
/// in case the initial "call" message arrives delayed.
|
||||
///
|
||||
/// For the caller, this means they should also not wait longer,
|
||||
/// as the callee won't start the call afterwards.
|
||||
const RINGING_SECONDS: i64 = 60;
|
||||
|
||||
// For persisting parameters in the call, we use Param::Arg*
|
||||
|
||||
const CALL_ACCEPTED_TIMESTAMP: Param = Param::Arg;
|
||||
const CALL_ENDED_TIMESTAMP: Param = Param::Arg4;
|
||||
|
||||
const STUN_PORT: u16 = 3478;
|
||||
|
||||
/// Set if incoming call was ended explicitly
|
||||
/// by the other side before we accepted it.
|
||||
///
|
||||
/// It is used to distinguish "ended" calls
|
||||
/// that are rejected by us from the calls
|
||||
/// canceled by the other side
|
||||
/// immediately after ringing started.
|
||||
const CALL_CANCELED_TIMESTAMP: Param = Param::Arg2;
|
||||
|
||||
/// Information about the status of a call.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CallInfo {
|
||||
/// User-defined text as given to place_outgoing_call()
|
||||
pub place_call_info: String,
|
||||
|
||||
/// User-defined text as given to accept_incoming_call()
|
||||
pub accept_call_info: String,
|
||||
|
||||
/// Message referring to the call.
|
||||
/// Data are persisted along with the message using Param::Arg*
|
||||
pub msg: Message,
|
||||
}
|
||||
|
||||
impl CallInfo {
|
||||
/// Returns true if the call is an incoming call.
|
||||
pub fn is_incoming(&self) -> bool {
|
||||
self.msg.from_id != ContactId::SELF
|
||||
}
|
||||
|
||||
/// Returns true if the call should not ring anymore.
|
||||
pub fn is_stale(&self) -> bool {
|
||||
(self.is_incoming() || self.msg.timestamp_sent != 0) && self.remaining_ring_seconds() <= 0
|
||||
}
|
||||
|
||||
fn remaining_ring_seconds(&self) -> i64 {
|
||||
let remaining_seconds = self.msg.timestamp_sent + RINGING_SECONDS - time();
|
||||
remaining_seconds.clamp(0, RINGING_SECONDS)
|
||||
}
|
||||
|
||||
async fn update_text(&self, context: &Context, text: &str) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET txt=?, txt_normalized=? WHERE id=?",
|
||||
(text, message::normalize_text(text), self.msg.id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_text_duration(&self, context: &Context) -> Result<()> {
|
||||
let minutes = self.duration_seconds() / 60;
|
||||
let duration = match minutes {
|
||||
0 => "<1 minute".to_string(),
|
||||
1 => "1 minute".to_string(),
|
||||
n => format!("{n} minutes"),
|
||||
};
|
||||
|
||||
if self.is_incoming() {
|
||||
self.update_text(context, &format!("Incoming call\n{duration}"))
|
||||
.await?;
|
||||
} else {
|
||||
self.update_text(context, &format!("Outgoing call\n{duration}"))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark calls as accepted.
|
||||
/// This is needed for all devices where a stale-timer runs, to prevent accepted calls being terminated as stale.
|
||||
async fn mark_as_accepted(&mut self, context: &Context) -> Result<()> {
|
||||
self.msg.param.set_i64(CALL_ACCEPTED_TIMESTAMP, time());
|
||||
self.msg.update_param(context).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if the call is accepted.
|
||||
pub fn is_accepted(&self) -> bool {
|
||||
self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP)
|
||||
}
|
||||
|
||||
/// Returns true if the call is missed
|
||||
/// because the caller canceled it
|
||||
/// explicitly before ringing stopped.
|
||||
///
|
||||
/// For outgoing calls this means
|
||||
/// the receiver has rejected the call
|
||||
/// explicitly.
|
||||
pub fn is_canceled(&self) -> bool {
|
||||
self.msg.param.exists(CALL_CANCELED_TIMESTAMP)
|
||||
}
|
||||
|
||||
async fn mark_as_ended(&mut self, context: &Context) -> Result<()> {
|
||||
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, time());
|
||||
self.msg.update_param(context).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Explicitly mark the call as canceled.
|
||||
///
|
||||
/// For incoming calls this should be called
|
||||
/// when "call ended" message is received
|
||||
/// from the caller before we picked up the call.
|
||||
/// In this case the call becomes "missed" early
|
||||
/// before the ringing timeout.
|
||||
async fn mark_as_canceled(&mut self, context: &Context) -> Result<()> {
|
||||
let now = time();
|
||||
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, now);
|
||||
self.msg.param.set_i64(CALL_CANCELED_TIMESTAMP, now);
|
||||
self.msg.update_param(context).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if the call is ended.
|
||||
pub fn is_ended(&self) -> bool {
|
||||
self.msg.param.exists(CALL_ENDED_TIMESTAMP)
|
||||
}
|
||||
|
||||
/// Returns call duration in seconds.
|
||||
pub fn duration_seconds(&self) -> i64 {
|
||||
if let (Some(start), Some(end)) = (
|
||||
self.msg.param.get_i64(CALL_ACCEPTED_TIMESTAMP),
|
||||
self.msg.param.get_i64(CALL_ENDED_TIMESTAMP),
|
||||
) {
|
||||
let seconds = end - start;
|
||||
if seconds <= 0 {
|
||||
return 1;
|
||||
}
|
||||
return seconds;
|
||||
}
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Start an outgoing call.
|
||||
pub async fn place_outgoing_call(
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
place_call_info: String,
|
||||
) -> Result<MsgId> {
|
||||
let chat = Chat::load_from_db(self, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Single,
|
||||
"Can only place calls in 1:1 chats"
|
||||
);
|
||||
ensure!(!chat.is_self_talk(), "Cannot call self");
|
||||
|
||||
let mut call = Message {
|
||||
viewtype: Viewtype::Call,
|
||||
text: "Outgoing call".into(),
|
||||
..Default::default()
|
||||
};
|
||||
call.param.set(Param::WebrtcRoom, &place_call_info);
|
||||
call.id = send_msg(self, chat_id, &mut call).await?;
|
||||
|
||||
let wait = RINGING_SECONDS;
|
||||
task::spawn(Context::emit_end_call_if_unaccepted(
|
||||
self.clone(),
|
||||
wait.try_into()?,
|
||||
call.id,
|
||||
));
|
||||
|
||||
Ok(call.id)
|
||||
}
|
||||
|
||||
/// Accept an incoming call.
|
||||
pub async fn accept_incoming_call(
|
||||
&self,
|
||||
call_id: MsgId,
|
||||
accept_call_info: String,
|
||||
) -> Result<()> {
|
||||
let mut call: CallInfo = self.load_call_by_id(call_id).await?.with_context(|| {
|
||||
format!("accept_incoming_call is called with {call_id} which does not refer to a call")
|
||||
})?;
|
||||
ensure!(call.is_incoming());
|
||||
if call.is_accepted() || call.is_ended() {
|
||||
info!(self, "Call already accepted/ended");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
call.mark_as_accepted(self).await?;
|
||||
let chat = Chat::load_from_db(self, call.msg.chat_id).await?;
|
||||
if chat.is_contact_request() {
|
||||
chat.id.accept(self).await?;
|
||||
}
|
||||
|
||||
// send an acceptance message around: to the caller as well as to the other devices of the callee
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: "[Call accepted]".into(),
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(SystemMessage::CallAccepted);
|
||||
msg.hidden = true;
|
||||
msg.param
|
||||
.set(Param::WebrtcAccepted, accept_call_info.to_string());
|
||||
msg.set_quote(self, Some(&call.msg)).await?;
|
||||
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
|
||||
self.emit_event(EventType::IncomingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
});
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancel, decline or hangup an incoming or outgoing call.
|
||||
pub async fn end_call(&self, call_id: MsgId) -> Result<()> {
|
||||
let mut call: CallInfo = self.load_call_by_id(call_id).await?.with_context(|| {
|
||||
format!("end_call is called with {call_id} which does not refer to a call")
|
||||
})?;
|
||||
if call.is_ended() {
|
||||
info!(self, "Call already ended");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !call.is_accepted() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
} else {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.update_text(self, "Canceled call").await?;
|
||||
}
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text_duration(self).await?;
|
||||
}
|
||||
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: "[Call ended]".into(),
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(SystemMessage::CallEnded);
|
||||
msg.hidden = true;
|
||||
msg.set_quote(self, Some(&call.msg)).await?;
|
||||
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
|
||||
|
||||
self.emit_event(EventType::CallEnded {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
});
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn emit_end_call_if_unaccepted(
|
||||
context: Context,
|
||||
wait: u64,
|
||||
call_id: MsgId,
|
||||
) -> Result<()> {
|
||||
sleep(Duration::from_secs(wait)).await;
|
||||
let Some(mut call) = context.load_call_by_id(call_id).await? else {
|
||||
warn!(
|
||||
context,
|
||||
"emit_end_call_if_unaccepted is called with {call_id} which does not refer to a call."
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
if !call.is_accepted() && !call.is_ended() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_canceled(&context).await?;
|
||||
call.update_text(&context, "Missed call").await?;
|
||||
} else {
|
||||
call.mark_as_ended(&context).await?;
|
||||
call.update_text(&context, "Canceled call").await?;
|
||||
}
|
||||
context.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
context.emit_event(EventType::CallEnded {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_call_msg(
|
||||
&self,
|
||||
call_id: MsgId,
|
||||
mime_message: &MimeMessage,
|
||||
from_id: ContactId,
|
||||
) -> Result<()> {
|
||||
if mime_message.is_call() {
|
||||
let Some(call) = self.load_call_by_id(call_id).await? else {
|
||||
warn!(self, "{call_id} does not refer to a call message");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if call.is_incoming() {
|
||||
if call.is_stale() {
|
||||
call.update_text(self, "Missed call").await?;
|
||||
self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
|
||||
} else {
|
||||
call.update_text(self, "Incoming call").await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
|
||||
let has_video = match sdp_has_video(&call.place_call_info) {
|
||||
Ok(has_video) => has_video,
|
||||
Err(err) => {
|
||||
warn!(self, "Failed to determine if SDP offer has video: {err:#}.");
|
||||
false
|
||||
}
|
||||
};
|
||||
if let Some(chat_id_blocked) =
|
||||
ChatIdBlocked::lookup_by_contact(self, from_id).await?
|
||||
{
|
||||
match chat_id_blocked.blocked {
|
||||
Blocked::Not => {
|
||||
self.emit_event(EventType::IncomingCall {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
place_call_info: call.place_call_info.to_string(),
|
||||
has_video,
|
||||
});
|
||||
}
|
||||
Blocked::Yes | Blocked::Request => {
|
||||
// Do not notify about incoming calls
|
||||
// from contact requests and blocked contacts.
|
||||
//
|
||||
// User can still access the call and accept it
|
||||
// via the chat in case of contact requests.
|
||||
}
|
||||
}
|
||||
}
|
||||
let wait = call.remaining_ring_seconds();
|
||||
task::spawn(Context::emit_end_call_if_unaccepted(
|
||||
self.clone(),
|
||||
wait.try_into()?,
|
||||
call.msg.id,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
call.update_text(self, "Outgoing call").await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
}
|
||||
} else {
|
||||
match mime_message.is_system_message {
|
||||
SystemMessage::CallAccepted => {
|
||||
let Some(mut call) = self.load_call_by_id(call_id).await? else {
|
||||
warn!(self, "{call_id} does not refer to a call message");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if call.is_ended() || call.is_accepted() {
|
||||
info!(self, "CallAccepted received for accepted/ended call");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
call.mark_as_accepted(self).await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
if call.is_incoming() {
|
||||
self.emit_event(EventType::IncomingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
});
|
||||
} else {
|
||||
let accept_call_info = mime_message
|
||||
.get_header(HeaderDef::ChatWebrtcAccepted)
|
||||
.unwrap_or_default();
|
||||
self.emit_event(EventType::OutgoingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
accept_call_info: accept_call_info.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
SystemMessage::CallEnded => {
|
||||
let Some(mut call) = self.load_call_by_id(call_id).await? else {
|
||||
warn!(self, "{call_id} does not refer to a call message");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if call.is_ended() {
|
||||
// may happen eg. if a a message is missed
|
||||
info!(self, "CallEnded received for ended call");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !call.is_accepted() {
|
||||
if call.is_incoming() {
|
||||
if from_id == ContactId::SELF {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
} else {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.update_text(self, "Missed call").await?;
|
||||
}
|
||||
} else {
|
||||
// outgoing
|
||||
if from_id == ContactId::SELF {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.update_text(self, "Canceled call").await?;
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text_duration(self).await?;
|
||||
}
|
||||
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
self.emit_event(EventType::CallEnded {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Loads information about the call given its ID.
|
||||
///
|
||||
/// If the message referred to by ID is
|
||||
/// not a call message, returns `None`.
|
||||
pub async fn load_call_by_id(&self, call_id: MsgId) -> Result<Option<CallInfo>> {
|
||||
let call = Message::load_from_db(self, call_id).await?;
|
||||
Ok(self.load_call_by_message(call))
|
||||
}
|
||||
|
||||
// Loads information about the call given the `Message`.
|
||||
//
|
||||
// If the `Message` is not a call message, returns `None`
|
||||
fn load_call_by_message(&self, call: Message) -> Option<CallInfo> {
|
||||
if call.viewtype != Viewtype::Call {
|
||||
// This can happen e.g. if a "call accepted"
|
||||
// or "call ended" message is received
|
||||
// with `In-Reply-To` referring to non-call message.
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(CallInfo {
|
||||
place_call_info: call
|
||||
.param
|
||||
.get(Param::WebrtcRoom)
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
accept_call_info: call
|
||||
.param
|
||||
.get(Param::WebrtcAccepted)
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
msg: call,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if SDP offer has a video.
|
||||
pub fn sdp_has_video(sdp: &str) -> Result<bool> {
|
||||
let mut cursor = Cursor::new(sdp);
|
||||
let session_description =
|
||||
SessionDescription::unmarshal(&mut cursor).context("Failed to parse SDP")?;
|
||||
for media_description in &session_description.media_descriptions {
|
||||
if media_description.media_name.media == "video" {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// State of the call for display in the message bubble.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum CallState {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// Returns call state given the message ID.
|
||||
///
|
||||
/// Returns an error if the message is not a call message.
|
||||
pub async fn call_state(context: &Context, msg_id: MsgId) -> Result<CallState> {
|
||||
let call = context
|
||||
.load_call_by_id(msg_id)
|
||||
.await?
|
||||
.with_context(|| format!("{msg_id} is not a call message"))?;
|
||||
let state = if call.is_incoming() {
|
||||
if call.is_accepted() {
|
||||
if call.is_ended() {
|
||||
CallState::Completed {
|
||||
duration: call.duration_seconds(),
|
||||
}
|
||||
} else {
|
||||
CallState::Active
|
||||
}
|
||||
} else if call.is_canceled() {
|
||||
// Call was explicitly canceled
|
||||
// by the caller before we picked it up.
|
||||
CallState::Missed
|
||||
} else if call.is_ended() {
|
||||
CallState::Declined
|
||||
} else if call.is_stale() {
|
||||
CallState::Missed
|
||||
} else {
|
||||
CallState::Alerting
|
||||
}
|
||||
} else if call.is_accepted() {
|
||||
if call.is_ended() {
|
||||
CallState::Completed {
|
||||
duration: call.duration_seconds(),
|
||||
}
|
||||
} else {
|
||||
CallState::Active
|
||||
}
|
||||
} else if call.is_canceled() {
|
||||
CallState::Canceled
|
||||
} else if call.is_ended() || call.is_stale() {
|
||||
CallState::Declined
|
||||
} else {
|
||||
CallState::Alerting
|
||||
};
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
/// ICE server for JSON serialization.
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
struct IceServer {
|
||||
/// STUN or TURN URLs.
|
||||
pub urls: Vec<String>,
|
||||
|
||||
/// Username for TURN server authentication.
|
||||
pub username: Option<String>,
|
||||
|
||||
/// Password for logging into the server.
|
||||
pub credential: Option<String>,
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers.
|
||||
async fn create_ice_servers(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<String> {
|
||||
// Do not use cache because there is no TLS.
|
||||
let load_cache = false;
|
||||
let urls: Vec<String> = lookup_host_with_cache(context, hostname, port, "", load_cache)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|addr| format!("turn:{addr}"))
|
||||
.collect();
|
||||
|
||||
let ice_server = IceServer {
|
||||
urls,
|
||||
username: Some(username.to_string()),
|
||||
credential: Some(password.to_string()),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&[ice_server])?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers from a line received over IMAP METADATA.
|
||||
///
|
||||
/// IMAP METADATA returns a line such as
|
||||
/// `example.com:3478:1758650868:8Dqkyyu11MVESBqjbIylmB06rv8=`
|
||||
///
|
||||
/// 1758650868 is the username and expiration timestamp
|
||||
/// at the same time,
|
||||
/// while `8Dqkyyu11MVESBqjbIylmB06rv8=`
|
||||
/// is the password.
|
||||
pub(crate) async fn create_ice_servers_from_metadata(
|
||||
context: &Context,
|
||||
metadata: &str,
|
||||
) -> Result<(i64, String)> {
|
||||
let (hostname, rest) = metadata.split_once(':').context("Missing hostname")?;
|
||||
let (port, rest) = rest.split_once(':').context("Missing port")?;
|
||||
let port = u16::from_str(port).context("Failed to parse the port")?;
|
||||
let (ts, password) = rest.split_once(':').context("Missing timestamp")?;
|
||||
let expiration_timestamp = i64::from_str(ts).context("Failed to parse the timestamp")?;
|
||||
let ice_servers = create_ice_servers(context, hostname, port, ts, password).await?;
|
||||
Ok((expiration_timestamp, ice_servers))
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers when no TURN servers are known.
|
||||
pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<String> {
|
||||
// Do not use public STUN server from https://stunprotocol.org/.
|
||||
// It changes the hostname every year
|
||||
// (e.g. stunserver2025.stunprotocol.org
|
||||
// which was previously stunserver2024.stunprotocol.org)
|
||||
// because of bandwidth costs:
|
||||
// <https://github.com/jselbie/stunserver/issues/50>
|
||||
|
||||
// We use nine.testrun.org for a default STUN server.
|
||||
let hostname = "nine.testrun.org";
|
||||
|
||||
// Do not use cache because there is no TLS.
|
||||
let load_cache = false;
|
||||
let urls: Vec<String> = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|addr| format!("stun:{addr}"))
|
||||
.collect();
|
||||
|
||||
let ice_server = IceServer {
|
||||
urls,
|
||||
username: None,
|
||||
credential: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&[ice_server])?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Returns JSON with ICE servers.
|
||||
///
|
||||
/// <https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers>
|
||||
///
|
||||
/// All returned servers are resolved to their IP addresses.
|
||||
/// The primary point of DNS lookup is that Delta Chat Desktop
|
||||
/// relies on the servers being specified by IP,
|
||||
/// because it itself cannot utilize DNS. See
|
||||
/// <https://github.com/deltachat/deltachat-desktop/issues/5447>.
|
||||
pub async fn ice_servers(context: &Context) -> Result<String> {
|
||||
if let Some(ref metadata) = *context.metadata.read().await {
|
||||
Ok(metadata.ice_servers.clone())
|
||||
} else {
|
||||
Ok("[]".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod calls_tests;
|
||||
@@ -1,674 +0,0 @@
|
||||
use super::*;
|
||||
use crate::chat::forward_msgs;
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
struct CallSetup {
|
||||
pub alice: TestContext,
|
||||
pub alice2: TestContext,
|
||||
pub alice_call: Message,
|
||||
pub alice2_call: Message,
|
||||
pub bob: TestContext,
|
||||
pub bob2: TestContext,
|
||||
pub bob_call: Message,
|
||||
pub bob2_call: Message,
|
||||
}
|
||||
|
||||
async fn assert_text(t: &TestContext, call_id: MsgId, text: &str) -> Result<()> {
|
||||
assert_eq!(Message::load_from_db(t, call_id).await?.text, text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Offer and answer examples from <https://www.rfc-editor.org/rfc/rfc3264>
|
||||
const PLACE_INFO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP4 host.anywhere.com\r\ns=-\r\nc=IN IP4 host.anywhere.com\r\nt=0 0\r\nm=audio 62986 RTP/AVP 0 4 18\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=rtpmap:18 G729/8000\r\na=inactive\r\n";
|
||||
const ACCEPT_INFO: &str = "v=0\r\no=bob 2890844730 2890844731 IN IP4 host.example.com\r\ns=\r\nc=IN IP4 host.example.com\r\nt=0 0\r\nm=audio 54344 RTP/AVP 0 4\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=inactive\r\n";
|
||||
|
||||
/// 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=-`.
|
||||
const PLACE_INFO_VIDEO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP6 2001:db8::3\r\ns=-\r\nc=IN IP6 2001:db8::3\r\nt=0 0\r\na=group:BUNDLE foo bar\r\n\r\nm=audio 10000 RTP/AVP 0 8 97\r\nb=AS:200\r\na=mid:foo\r\na=rtcp-mux\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:97 iLBC/8000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\r\nm=video 10002 RTP/AVP 31 32\r\nb=AS:1000\r\na=mid:bar\r\na=rtcp-mux\r\na=rtpmap:31 H261/90000\r\na=rtpmap:32 MPV/90000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n";
|
||||
|
||||
async fn setup_call() -> Result<CallSetup> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let alice2 = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let bob2 = tcm.bob().await;
|
||||
for t in [&alice, &alice2, &bob, &bob2] {
|
||||
t.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
|
||||
// Alice creates a chat with Bob and places an outgoing call there.
|
||||
// Alice's other device sees the same message as an outgoing call.
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
// Create chat on Bob's side
|
||||
// so incoming call causes a notification.
|
||||
bob.create_chat(&alice).await;
|
||||
bob2.create_chat(&alice).await;
|
||||
|
||||
let test_msg_id = alice
|
||||
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string())
|
||||
.await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
assert_eq!(sent1.sender_msg_id, test_msg_id);
|
||||
let alice_call = Message::load_from_db(&alice, sent1.sender_msg_id).await?;
|
||||
let alice2_call = alice2.recv_msg(&sent1).await;
|
||||
for (t, m) in [(&alice, &alice_call), (&alice2, &alice2_call)] {
|
||||
assert!(!m.is_info());
|
||||
assert_eq!(m.viewtype, Viewtype::Call);
|
||||
let info = t
|
||||
.load_call_by_id(m.id)
|
||||
.await?
|
||||
.expect("m should be a call message");
|
||||
assert!(!info.is_incoming());
|
||||
assert!(!info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_text(t, m.id, "Outgoing call").await?;
|
||||
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
|
||||
}
|
||||
|
||||
// Bob receives the message referring to the call on two devices;
|
||||
// it is an incoming call from the view of Bob
|
||||
let bob_call = bob.recv_msg(&sent1).await;
|
||||
let bob2_call = bob2.recv_msg(&sent1).await;
|
||||
for (t, m) in [(&bob, &bob_call), (&bob2, &bob2_call)] {
|
||||
assert!(!m.is_info());
|
||||
assert_eq!(m.viewtype, Viewtype::Call);
|
||||
t.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCall { .. }))
|
||||
.await;
|
||||
let info = t
|
||||
.load_call_by_id(m.id)
|
||||
.await?
|
||||
.expect("IncomingCall event should refer to a call message");
|
||||
assert!(info.is_incoming());
|
||||
assert!(!info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_text(t, m.id, "Incoming call").await?;
|
||||
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
|
||||
}
|
||||
|
||||
Ok(CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
})
|
||||
}
|
||||
|
||||
async fn accept_call() -> Result<CallSetup> {
|
||||
let CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
} = setup_call().await?;
|
||||
|
||||
// Bob accepts the incoming call
|
||||
bob.accept_incoming_call(bob_call.id, ACCEPT_INFO.to_string())
|
||||
.await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.await;
|
||||
let sent2 = bob.pop_sent_msg().await;
|
||||
let info = bob
|
||||
.load_call_by_id(bob_call.id)
|
||||
.await?
|
||||
.expect("bob_call should be a call message");
|
||||
assert!(info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Active);
|
||||
|
||||
bob2.recv_msg_trash(&sent2).await;
|
||||
assert_text(&bob, bob_call.id, "Incoming call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.await;
|
||||
let info = bob2
|
||||
.load_call_by_id(bob2_call.id)
|
||||
.await?
|
||||
.expect("bob2_call should be a call message");
|
||||
assert!(info.is_accepted());
|
||||
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Active);
|
||||
|
||||
// Alice receives the acceptance message
|
||||
alice.recv_msg_trash(&sent2).await;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call").await?;
|
||||
let ev = alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
|
||||
.await;
|
||||
assert_eq!(
|
||||
ev,
|
||||
EventType::OutgoingCallAccepted {
|
||||
msg_id: alice_call.id,
|
||||
chat_id: alice_call.chat_id,
|
||||
accept_call_info: ACCEPT_INFO.to_string()
|
||||
}
|
||||
);
|
||||
let info = alice
|
||||
.load_call_by_id(alice_call.id)
|
||||
.await?
|
||||
.expect("alice_call should be a call message");
|
||||
assert!(info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_eq!(call_state(&alice, alice_call.id).await?, CallState::Active);
|
||||
|
||||
alice2.recv_msg_trash(&sent2).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
|
||||
.await;
|
||||
assert_eq!(
|
||||
call_state(&alice2, alice2_call.id).await?,
|
||||
CallState::Active
|
||||
);
|
||||
|
||||
Ok(CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
// Alice calls Bob, Bob accepts
|
||||
let CallSetup {
|
||||
alice,
|
||||
alice_call,
|
||||
alice2,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
..
|
||||
} = accept_call().await?;
|
||||
|
||||
// Bob has accepted the call and also ends it
|
||||
bob.end_call(bob_call.id).await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
let sent3 = bob.pop_sent_msg().await;
|
||||
assert!(matches!(
|
||||
call_state(&bob, bob_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert!(matches!(
|
||||
call_state(&bob2, bob2_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
// Alice receives the ending message
|
||||
alice.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert!(matches!(
|
||||
call_state(&alice, alice_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert!(matches!(
|
||||
call_state(&alice2, alice2_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
// Alice calls Bob, Bob accepts
|
||||
let CallSetup {
|
||||
alice,
|
||||
alice_call,
|
||||
alice2,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
..
|
||||
} = accept_call().await?;
|
||||
|
||||
// Bob has accepted the call but Alice ends it
|
||||
alice.end_call(alice_call.id).await?;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
let sent3 = alice.pop_sent_msg().await;
|
||||
assert!(matches!(
|
||||
call_state(&alice, alice_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert!(matches!(
|
||||
call_state(&alice2, alice2_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
// Bob receives the ending message
|
||||
bob.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert!(matches!(
|
||||
call_state(&bob, bob_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert!(matches!(
|
||||
call_state(&bob2, bob2_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_callee_rejects_call() -> Result<()> {
|
||||
// Alice calls Bob
|
||||
let CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
..
|
||||
} = setup_call().await?;
|
||||
|
||||
// Bob has accepted Alice before, but does not want to talk with Alice
|
||||
bob_call.chat_id.accept(&bob).await?;
|
||||
bob.end_call(bob_call.id).await?;
|
||||
assert_text(&bob, bob_call.id, "Declined call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
let sent3 = bob.pop_sent_msg().await;
|
||||
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Declined);
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Declined call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Declined);
|
||||
|
||||
// Alice receives decline message
|
||||
alice.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice, alice_call.id, "Declined call").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert_eq!(
|
||||
call_state(&alice, alice_call.id).await?,
|
||||
CallState::Declined
|
||||
);
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Declined call").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert_eq!(
|
||||
call_state(&alice2, alice2_call.id).await?,
|
||||
CallState::Declined
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_caller_cancels_call() -> Result<()> {
|
||||
// Alice calls Bob
|
||||
let CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
..
|
||||
} = setup_call().await?;
|
||||
|
||||
// Alice changes their mind before Bob picks up
|
||||
alice.end_call(alice_call.id).await?;
|
||||
assert_text(&alice, alice_call.id, "Canceled call").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
let sent3 = alice.pop_sent_msg().await;
|
||||
assert_eq!(
|
||||
call_state(&alice, alice_call.id).await?,
|
||||
CallState::Canceled
|
||||
);
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Canceled call").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert_eq!(
|
||||
call_state(&alice2, alice2_call.id).await?,
|
||||
CallState::Canceled
|
||||
);
|
||||
|
||||
// Bob receives the ending message
|
||||
bob.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob, bob_call.id, "Missed call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Missed);
|
||||
|
||||
// Test that message summary says it is a missed call.
|
||||
let bob_call_msg = Message::load_from_db(&bob, bob_call.id).await?;
|
||||
let summary = bob_call_msg.get_summary(&bob, None).await?;
|
||||
assert_eq!(summary.text, "📞 Missed call");
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Missed call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Missed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_is_stale_call() -> Result<()> {
|
||||
// a call started now is not stale
|
||||
let call_info = CallInfo {
|
||||
msg: Message {
|
||||
timestamp_sent: time(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!call_info.is_stale());
|
||||
let remaining_seconds = call_info.remaining_ring_seconds();
|
||||
assert!(remaining_seconds == RINGING_SECONDS || remaining_seconds == RINGING_SECONDS - 1);
|
||||
|
||||
// call started 5 seconds ago, this is not stale as well
|
||||
let call_info = CallInfo {
|
||||
msg: Message {
|
||||
timestamp_sent: time() - 5,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!call_info.is_stale());
|
||||
let remaining_seconds = call_info.remaining_ring_seconds();
|
||||
assert!(remaining_seconds == RINGING_SECONDS - 5 || remaining_seconds == RINGING_SECONDS - 6);
|
||||
|
||||
// a call started one hour ago is clearly stale
|
||||
let call_info = CallInfo {
|
||||
msg: Message {
|
||||
timestamp_sent: time() - 3600,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
assert!(call_info.is_stale());
|
||||
assert_eq!(call_info.remaining_ring_seconds(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mark_calls() -> Result<()> {
|
||||
let CallSetup {
|
||||
alice, alice_call, ..
|
||||
} = setup_call().await?;
|
||||
|
||||
let mut call_info: CallInfo = alice
|
||||
.load_call_by_id(alice_call.id)
|
||||
.await?
|
||||
.expect("alice_call should be a call message");
|
||||
assert!(!call_info.is_accepted());
|
||||
assert!(!call_info.is_ended());
|
||||
call_info.mark_as_accepted(&alice).await?;
|
||||
assert!(call_info.is_accepted());
|
||||
assert!(!call_info.is_ended());
|
||||
|
||||
let mut call_info: CallInfo = alice
|
||||
.load_call_by_id(alice_call.id)
|
||||
.await?
|
||||
.expect("alice_call should be a call message");
|
||||
assert!(call_info.is_accepted());
|
||||
assert!(!call_info.is_ended());
|
||||
|
||||
call_info.mark_as_ended(&alice).await?;
|
||||
assert!(call_info.is_accepted());
|
||||
assert!(call_info.is_ended());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_update_call_text() -> Result<()> {
|
||||
let CallSetup {
|
||||
alice, alice_call, ..
|
||||
} = setup_call().await?;
|
||||
|
||||
let call_info = alice
|
||||
.load_call_by_id(alice_call.id)
|
||||
.await?
|
||||
.expect("alice_call should be a call message");
|
||||
call_info.update_text(&alice, "foo bar").await?;
|
||||
|
||||
let alice_call = Message::load_from_db(&alice, alice_call.id).await?;
|
||||
assert_eq!(alice_call.get_text(), "foo bar");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sdp_has_video() {
|
||||
assert!(sdp_has_video("foobar").is_err());
|
||||
assert_eq!(sdp_has_video(PLACE_INFO).unwrap(), false);
|
||||
assert_eq!(sdp_has_video(PLACE_INFO_VIDEO).unwrap(), true);
|
||||
}
|
||||
|
||||
/// Tests that calls are forwarded as text messages.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forward_call() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let alice_bob_chat = alice.create_chat(bob).await;
|
||||
let alice_msg_id = alice
|
||||
.place_outgoing_call(alice_bob_chat.id, PLACE_INFO.to_string())
|
||||
.await
|
||||
.context("Failed to place a call")?;
|
||||
let alice_call = Message::load_from_db(alice, alice_msg_id).await?;
|
||||
|
||||
let _alice_sent_call = alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_call.viewtype, Viewtype::Call);
|
||||
|
||||
let alice_charlie_chat = alice.create_chat(charlie).await;
|
||||
forward_msgs(alice, &[alice_call.id], alice_charlie_chat.id).await?;
|
||||
let alice_forwarded_call = alice.pop_sent_msg().await;
|
||||
let alice_forwarded_call_msg = alice_forwarded_call.load_from_db().await;
|
||||
assert_eq!(alice_forwarded_call_msg.viewtype, Viewtype::Text);
|
||||
|
||||
let charlie_forwarded_call = charlie.recv_msg(&alice_forwarded_call).await;
|
||||
assert_eq!(charlie_forwarded_call.viewtype, Viewtype::Text);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that "end call" message referring
|
||||
/// to a text message does not make receive_imf fail.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_end_text_call() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let received1 = receive_imf(
|
||||
alice,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <first@example.net>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
\n\
|
||||
Hello\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(received1.msg_ids.len(), 1);
|
||||
let msg = Message::load_from_db(alice, received1.msg_ids[0])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.viewtype, Viewtype::Text);
|
||||
|
||||
// Receiving "Call ended" message that refers
|
||||
// to the text message does not result in an error.
|
||||
let received2 = receive_imf(
|
||||
alice,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <second@example.net>\n\
|
||||
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
|
||||
In-Reply-To: <first@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Content: call-ended\n\
|
||||
\n\
|
||||
Call ended\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(received2.msg_ids.len(), 1);
|
||||
assert_eq!(received2.chat_id, DC_CHAT_ID_TRASH);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that partially downloaded "call ended"
|
||||
/// messages are not processed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_partial_calls() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let seen = false;
|
||||
|
||||
// The messages in the test
|
||||
// have no `Date` on purpose,
|
||||
// so they are treated as new.
|
||||
let received_call = receive_imf(
|
||||
alice,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <first@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Content: call\n\
|
||||
Chat-Webrtc-Room: YWFhYWFhYWFhCg==\n\
|
||||
\n\
|
||||
Hello, this is a call\n",
|
||||
seen,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(received_call.msg_ids.len(), 1);
|
||||
let call_msg = Message::load_from_db(alice, received_call.msg_ids[0])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(call_msg.viewtype, Viewtype::Call);
|
||||
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
|
||||
|
||||
let imf_raw = b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <second@example.net>\n\
|
||||
In-Reply-To: <first@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Content: call-ended\n\
|
||||
\n\
|
||||
Call ended\n";
|
||||
receive_imf_from_inbox(
|
||||
alice,
|
||||
"second@example.net",
|
||||
imf_raw,
|
||||
seen,
|
||||
Some(imf_raw.len().try_into().unwrap()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// The call is still not ended.
|
||||
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
|
||||
|
||||
// Fully downloading the message ends the call.
|
||||
receive_imf_from_inbox(alice, "second@example.net", imf_raw, seen, None)
|
||||
.await
|
||||
.context("Failed to fully download end call message")?;
|
||||
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Missed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
1435
src/chat.rs
1435
src/chat.rs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
//! # Chat list module.
|
||||
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::chat::{Chat, ChatId, ChatVisibility, update_special_chat_names};
|
||||
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
|
||||
use crate::constants::{
|
||||
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_GCL_ADD_ALLDONE_HINT,
|
||||
DC_GCL_ARCHIVED_ONLY, DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
|
||||
@@ -245,6 +245,9 @@ impl Chatlist {
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
};
|
||||
// Return ProtectionBroken chats also, as that may happen to a verified chat at any
|
||||
// time. It may be confusing if a chat that is normally in the list disappears
|
||||
// suddenly. The UI need to deal with that case anyway.
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, c.type, c.param, m.id
|
||||
FROM chats c
|
||||
@@ -406,8 +409,7 @@ impl Chatlist {
|
||||
if lastmsg.from_id == ContactId::SELF {
|
||||
None
|
||||
} else if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::OutBroadcast
|
||||
|| chat.typ == Chattype::InBroadcast
|
||||
|| chat.typ == Chattype::Broadcast
|
||||
|| chat.typ == Chattype::Mailinglist
|
||||
|| chat.is_self_talk()
|
||||
{
|
||||
@@ -481,23 +483,27 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::chat::save_msgs;
|
||||
use crate::chat::{
|
||||
add_contact_to_chat, create_group, get_chat_contacts, remove_contact_from_chat,
|
||||
send_text_msg,
|
||||
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
|
||||
send_text_msg, ProtectionStatus,
|
||||
};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::StockMessage;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools::SystemTime;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_try_load() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
let chat_id1 = create_group(bob, "a chat").await.unwrap();
|
||||
let chat_id2 = create_group(bob, "b chat").await.unwrap();
|
||||
let chat_id3 = create_group(bob, "c chat").await.unwrap();
|
||||
let chat_id1 = create_group_chat(bob, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id2 = create_group_chat(bob, ProtectionStatus::Unprotected, "b chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id3 = create_group_chat(bob, ProtectionStatus::Unprotected, "c chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// check that the chatlist starts with the most recent message
|
||||
let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap();
|
||||
@@ -506,8 +512,6 @@ mod tests {
|
||||
assert_eq!(chats.get_chat_id(1).unwrap(), chat_id2);
|
||||
assert_eq!(chats.get_chat_id(2).unwrap(), chat_id1);
|
||||
|
||||
SystemTime::shift(Duration::from_secs(5));
|
||||
|
||||
// New drafts are sorted to the top
|
||||
// We have to set a draft on the other two messages, too, as
|
||||
// chat timestamps are only exact to the second and sorting by timestamp
|
||||
@@ -530,7 +534,9 @@ mod tests {
|
||||
|
||||
// receive a message from alice
|
||||
let alice = &tcm.alice().await;
|
||||
let alice_chat_id = create_group(alice, "alice chat").await.unwrap();
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "alice chat")
|
||||
.await
|
||||
.unwrap();
|
||||
add_contact_to_chat(
|
||||
alice,
|
||||
alice_chat_id,
|
||||
@@ -566,29 +572,27 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sort_self_talk_up_on_forward() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = TestContext::new().await;
|
||||
t.update_device_chats().await.unwrap();
|
||||
create_group(&t, "a chat").await.unwrap();
|
||||
create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 3);
|
||||
assert!(
|
||||
!Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_self_talk()
|
||||
);
|
||||
assert!(!Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_self_talk());
|
||||
|
||||
let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chats.len(), 2); // device chat cannot be written and is skipped on forwarding
|
||||
assert!(
|
||||
Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_self_talk()
|
||||
);
|
||||
assert!(Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_self_talk());
|
||||
|
||||
remove_contact_from_chat(&t, chats.get_chat_id(1).unwrap(), ContactId::SELF)
|
||||
.await
|
||||
@@ -601,7 +605,7 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_special_chat_names() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = TestContext::new().await;
|
||||
t.update_device_chats().await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
|
||||
@@ -755,7 +759,9 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_summary_unwrap() {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id1 = create_group(&t, "a chat").await.unwrap();
|
||||
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut msg = Message::new_text("foo:\nbar \r\n test".to_string());
|
||||
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
@@ -771,7 +777,9 @@ mod tests {
|
||||
async fn test_get_summary_deleted_draft() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let chat_id = create_group(&t, "a chat").await.unwrap();
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let mut msg = Message::new_text("Foobar".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
|
||||
@@ -810,9 +818,15 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_broken() {
|
||||
let t = TestContext::new_bob().await;
|
||||
let chat_id1 = create_group(&t, "a chat").await.unwrap();
|
||||
create_group(&t, "b chat").await.unwrap();
|
||||
create_group(&t, "c chat").await.unwrap();
|
||||
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
|
||||
.await
|
||||
.unwrap();
|
||||
create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// check that the chatlist starts with the most recent message
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
|
||||
46
src/color.rs
46
src/color.rs
@@ -1,39 +1,38 @@
|
||||
//! Color generation.
|
||||
//! Implementation of Consistent Color Generation.
|
||||
//!
|
||||
//! This is similar to Consistent Color Generation defined in XEP-0392,
|
||||
//! but uses OKLCh colorspace instead of HSLuv
|
||||
//! to ensure that colors have the same lightness.
|
||||
use colorutils_rs::{Oklch, Rgb, TransferFunction};
|
||||
//! Consistent Color Generation is defined in XEP-0392.
|
||||
//!
|
||||
//! Color Vision Deficiency correction is not implemented as Delta Chat does not offer
|
||||
//! corresponding settings.
|
||||
use hsluv::hsluv_to_rgb;
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
/// Converts an identifier to Hue angle.
|
||||
fn str_to_angle(s: &str) -> f32 {
|
||||
fn str_to_angle(s: &str) -> f64 {
|
||||
let bytes = s.as_bytes();
|
||||
let result = Sha1::digest(bytes);
|
||||
let checksum: u16 = result.first().map_or(0, |&x| u16::from(x))
|
||||
+ 256 * result.get(1).map_or(0, |&x| u16::from(x));
|
||||
f32::from(checksum) / 65536.0 * 360.0
|
||||
f64::from(checksum) / 65536.0 * 360.0
|
||||
}
|
||||
|
||||
/// Converts RGB tuple to a 24-bit number.
|
||||
///
|
||||
/// Returns a 24-bit number with 8 least significant bits corresponding to the blue color and 8
|
||||
/// most significant bits corresponding to the red color.
|
||||
fn rgb_to_u32(rgb: Rgb<u8>) -> u32 {
|
||||
65536 * u32::from(rgb.r) + 256 * u32::from(rgb.g) + u32::from(rgb.b)
|
||||
fn rgb_to_u32((r, g, b): (f64, f64, f64)) -> u32 {
|
||||
let r = ((r * 256.0) as u32).min(255);
|
||||
let g = ((g * 256.0) as u32).min(255);
|
||||
let b = ((b * 256.0) as u32).min(255);
|
||||
65536 * r + 256 * g + b
|
||||
}
|
||||
|
||||
/// Converts an identifier to RGB color.
|
||||
///
|
||||
/// Lightness is set to half (0.5) to make colors suitable both for light and dark theme.
|
||||
/// Saturation is set to maximum (100.0) to make colors distinguishable, and lightness is set to
|
||||
/// half (50.0) to make colors suitable both for light and dark theme.
|
||||
pub fn str_to_color(s: &str) -> u32 {
|
||||
let lightness = 0.5;
|
||||
let chroma = 0.23;
|
||||
let angle = str_to_angle(s);
|
||||
let oklch = Oklch::new(lightness, chroma, angle);
|
||||
let rgb = oklch.to_rgb(TransferFunction::Srgb);
|
||||
|
||||
rgb_to_u32(rgb)
|
||||
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
|
||||
}
|
||||
|
||||
/// Returns color as a "#RRGGBB" `String` where R, G, B are hex digits.
|
||||
@@ -46,7 +45,6 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::excessive_precision)]
|
||||
fn test_str_to_angle() {
|
||||
// Test against test vectors from
|
||||
// <https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd>
|
||||
@@ -59,11 +57,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_rgb_to_u32() {
|
||||
assert_eq!(rgb_to_u32(Rgb::new(0, 0, 0)), 0);
|
||||
assert_eq!(rgb_to_u32(Rgb::new(0xff, 0xff, 0xff)), 0xffffff);
|
||||
assert_eq!(rgb_to_u32(Rgb::new(0, 0, 0xff)), 0x0000ff);
|
||||
assert_eq!(rgb_to_u32(Rgb::new(0, 0xff, 0)), 0x00ff00);
|
||||
assert_eq!(rgb_to_u32(Rgb::new(0xff, 0, 0)), 0xff0000);
|
||||
assert_eq!(rgb_to_u32(Rgb::new(0xff, 0x80, 0)), 0xff8000);
|
||||
assert_eq!(rgb_to_u32((0.0, 0.0, 0.0)), 0);
|
||||
assert_eq!(rgb_to_u32((1.0, 1.0, 1.0)), 0xffffff);
|
||||
assert_eq!(rgb_to_u32((0.0, 0.0, 1.0)), 0x0000ff);
|
||||
assert_eq!(rgb_to_u32((0.0, 1.0, 0.0)), 0x00ff00);
|
||||
assert_eq!(rgb_to_u32((1.0, 0.0, 0.0)), 0xff0000);
|
||||
assert_eq!(rgb_to_u32((1.0, 0.5, 0.0)), 0xff8000);
|
||||
}
|
||||
}
|
||||
|
||||
101
src/config.rs
101
src/config.rs
@@ -4,7 +4,7 @@ use std::env;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use deltachat_contact_tools::{addr_cmp, sanitize_single_line};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -14,15 +14,15 @@ use tokio::fs;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::configure::EnteredLoginParam;
|
||||
use crate::constants;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{LogExt, info};
|
||||
use crate::log::{info, LogExt};
|
||||
use crate::login_param::ConfiguredLoginParam;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::{Provider, get_provider_by_id};
|
||||
use crate::provider::{get_provider_by_id, Provider};
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::get_abs_path;
|
||||
use crate::{constants, stats};
|
||||
|
||||
/// The available configuration keys.
|
||||
#[derive(
|
||||
@@ -151,11 +151,19 @@ pub enum Config {
|
||||
/// setting up a second device, or receiving a sync message.
|
||||
BccSelf,
|
||||
|
||||
/// True if encryption is preferred according to Autocrypt standard.
|
||||
#[strum(props(default = "1"))]
|
||||
E2eeEnabled,
|
||||
|
||||
/// True if Message Delivery Notifications (read receipts) should
|
||||
/// be sent and requested.
|
||||
#[strum(props(default = "1"))]
|
||||
MdnsEnabled,
|
||||
|
||||
/// True if "Sent" folder should be watched for changes.
|
||||
#[strum(props(default = "0"))]
|
||||
SentboxWatch,
|
||||
|
||||
/// True if chat messages should be moved to a separate folder. Auto-sent messages like sync
|
||||
/// ones are moved there anyway.
|
||||
#[strum(props(default = "1"))]
|
||||
@@ -281,6 +289,9 @@ pub enum Config {
|
||||
/// Configured folder for chat messages.
|
||||
ConfiguredMvboxFolder,
|
||||
|
||||
/// Configured "Sent" folder.
|
||||
ConfiguredSentboxFolder,
|
||||
|
||||
/// Configured "Trash" folder.
|
||||
ConfiguredTrashFolder,
|
||||
|
||||
@@ -339,6 +350,9 @@ pub enum Config {
|
||||
/// Unset, when quota falls below minimal warning threshold again.
|
||||
QuotaExceeding,
|
||||
|
||||
/// address to webrtc instance to use for videochats
|
||||
WebrtcInstance,
|
||||
|
||||
/// Timestamp of the last time housekeeping was run
|
||||
LastHousekeeping,
|
||||
|
||||
@@ -355,9 +369,6 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
DisableIdle,
|
||||
|
||||
/// Timestamp of the next check for donation request need.
|
||||
DonationRequestNextCheck,
|
||||
|
||||
/// Defines the max. size (in bytes) of messages downloaded automatically.
|
||||
/// 0 = no limit.
|
||||
#[strum(props(default = "0"))]
|
||||
@@ -382,6 +393,12 @@ pub enum Config {
|
||||
/// Make all outgoing messages with Autocrypt header "multipart/signed".
|
||||
SignUnencrypted,
|
||||
|
||||
/// Enable header protection for `Autocrypt` header.
|
||||
///
|
||||
/// This is an experimental setting not compatible to other MUAs
|
||||
/// and older Delta Chat versions (core version <= v1.149.0).
|
||||
ProtectAutocrypt,
|
||||
|
||||
/// Let the core save all events to the database.
|
||||
/// This value is used internally to remember the MsgId of the logging xdc
|
||||
#[strum(props(default = "0"))]
|
||||
@@ -397,26 +414,23 @@ pub enum Config {
|
||||
#[strum(props(default = "172800"))]
|
||||
GossipPeriod,
|
||||
|
||||
/// Feature flag for verified 1:1 chats; the UI should set it
|
||||
/// to 1 if it supports verified 1:1 chats.
|
||||
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
|
||||
/// and when the key changes, an info message is posted into the chat.
|
||||
/// 0=Nothing else happens when the key changes.
|
||||
/// 1=After the key changed, `can_send()` returns false and `is_protection_broken()` returns true
|
||||
/// until `chat_id.accept()` is called.
|
||||
#[strum(props(default = "0"))]
|
||||
VerifiedOneOnOneChats,
|
||||
|
||||
/// Row ID of the key in the `keypairs` table
|
||||
/// used for signatures, encryption to self and included in `Autocrypt` header.
|
||||
KeyId,
|
||||
|
||||
/// Send statistics to Delta Chat's developers.
|
||||
/// Can be exposed to the user as a setting.
|
||||
StatsSending,
|
||||
|
||||
/// Last time statistics were sent to Delta Chat's developers
|
||||
StatsLastSent,
|
||||
|
||||
/// Last time `update_message_stats()` was called
|
||||
StatsLastUpdate,
|
||||
|
||||
/// This key is sent to the statistics bot so that the bot can recognize the user
|
||||
/// This key is sent to the self_reporting bot so that the bot can recognize the user
|
||||
/// without storing the email address
|
||||
StatsId,
|
||||
|
||||
/// The last contact id that already existed when statistics-sending was enabled for the first time.
|
||||
StatsLastOldContactId,
|
||||
SelfReportingId,
|
||||
|
||||
/// MsgId of webxdc map integration.
|
||||
WebxdcIntegration,
|
||||
@@ -437,9 +451,6 @@ pub enum Config {
|
||||
/// to avoid encrypting it differently and
|
||||
/// storing the same token multiple times on the server.
|
||||
EncryptedDeviceToken,
|
||||
|
||||
/// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests.
|
||||
FailOnReceivingFullMsg,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -466,7 +477,10 @@ impl Config {
|
||||
|
||||
/// Whether the config option needs an IO scheduler restart to take effect.
|
||||
pub(crate) fn needs_io_restart(&self) -> bool {
|
||||
matches!(self, Config::MvboxMove | Config::OnlyFetchMvbox)
|
||||
matches!(
|
||||
self,
|
||||
Config::MvboxMove | Config::OnlyFetchMvbox | Config::SentboxWatch
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,9 +593,8 @@ impl Context {
|
||||
/// Returns boolean configuration value for the given key.
|
||||
pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
|
||||
Ok(self
|
||||
.get_config(key)
|
||||
.get_config_parsed::<i32>(key)
|
||||
.await?
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
.map(|x| x != 0)
|
||||
.unwrap_or_default())
|
||||
}
|
||||
@@ -593,6 +606,15 @@ impl Context {
|
||||
|| !self.get_config_bool(Config::IsChatmail).await?)
|
||||
}
|
||||
|
||||
/// Returns true if sentbox ("Sent" folder) should be watched.
|
||||
pub(crate) async fn should_watch_sentbox(&self) -> Result<bool> {
|
||||
Ok(self.get_config_bool(Config::SentboxWatch).await?
|
||||
&& self
|
||||
.get_config(Config::ConfiguredSentboxFolder)
|
||||
.await?
|
||||
.is_some())
|
||||
}
|
||||
|
||||
/// Returns true if sync messages should be sent.
|
||||
pub(crate) async fn should_send_sync_msgs(&self) -> Result<bool> {
|
||||
Ok(self.get_config_bool(Config::SyncMsgs).await?
|
||||
@@ -680,7 +702,9 @@ impl Context {
|
||||
Config::Socks5Enabled
|
||||
| Config::ProxyEnabled
|
||||
| Config::BccSelf
|
||||
| Config::E2eeEnabled
|
||||
| Config::MdnsEnabled
|
||||
| Config::SentboxWatch
|
||||
| Config::MvboxMove
|
||||
| Config::OnlyFetchMvbox
|
||||
| Config::DeleteToTrash
|
||||
@@ -707,18 +731,12 @@ impl Context {
|
||||
Self::check_config(key, value)?;
|
||||
|
||||
let _pause = match key.needs_io_restart() {
|
||||
true => self.scheduler.pause(self).await?,
|
||||
true => self.scheduler.pause(self.clone()).await?,
|
||||
_ => Default::default(),
|
||||
};
|
||||
if key == Config::StatsSending {
|
||||
let old_value = self.get_config(key).await?;
|
||||
let old_value = bool_from_config(old_value.as_deref());
|
||||
let new_value = bool_from_config(value);
|
||||
stats::pre_sending_config_change(self, old_value, new_value).await?;
|
||||
}
|
||||
self.set_config_internal(key, value).await?;
|
||||
if key == Config::StatsSending {
|
||||
stats::maybe_send_stats(self).await?;
|
||||
if key == Config::SentboxWatch {
|
||||
self.last_full_folder_scan.lock().await.take();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -795,10 +813,7 @@ impl Context {
|
||||
bail!("Cannot change ConfiguredAddr");
|
||||
}
|
||||
if let Some(addr) = value {
|
||||
info!(
|
||||
self,
|
||||
"Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!"
|
||||
);
|
||||
info!(self, "Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!");
|
||||
ConfiguredLoginParam::from_json(&format!(
|
||||
r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#
|
||||
))?
|
||||
@@ -871,10 +886,6 @@ pub(crate) fn from_bool(val: bool) -> Option<&'static str> {
|
||||
Some(if val { "1" } else { "0" })
|
||||
}
|
||||
|
||||
pub(crate) fn bool_from_config(config: Option<&str>) -> bool {
|
||||
config.is_some_and(|v| v.parse::<i32>().unwrap_or_default() != 0)
|
||||
}
|
||||
|
||||
// Separate impl block for self address handling
|
||||
impl Context {
|
||||
/// Determine whether the specified addr maps to the/a self addr.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user