mirror of
https://github.com/chatmail/core.git
synced 2026-04-05 15:02:11 +03:00
Compare commits
321 Commits
v2.3.0
...
iequidoo/u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
247ec1e1ad | ||
|
|
1eb44328f0 | ||
|
|
0e30dd895f | ||
|
|
c001a9a983 | ||
|
|
5f3948b462 | ||
|
|
45a1d81805 | ||
|
|
19d7799324 | ||
|
|
24e18c1485 | ||
|
|
3eb1a7dfac | ||
|
|
2f2a147efb | ||
|
|
f4938465c3 | ||
|
|
129137b5de | ||
|
|
ec3f765727 | ||
|
|
a743ad9490 | ||
|
|
89315b8ef2 | ||
|
|
e7348a4fd8 | ||
|
|
c68244692d | ||
|
|
3c93f61b4d | ||
|
|
51b9e86d71 | ||
|
|
347938a9f9 | ||
|
|
9897ef2e9b | ||
|
|
2f34a740c7 | ||
|
|
fc81cef113 | ||
|
|
04c2585c27 | ||
|
|
59fac54f7b | ||
|
|
65b61efb31 | ||
|
|
afc74b0829 | ||
|
|
2481a0f48e | ||
|
|
6c24edb40d | ||
|
|
e4178789da | ||
|
|
b417ba86bc | ||
|
|
498a831873 | ||
|
|
c6722d36de | ||
|
|
90f0d5c060 | ||
|
|
90ec2f2518 | ||
|
|
5b66535134 | ||
|
|
eea848f72b | ||
|
|
214a1d3e2d | ||
|
|
e270a502d1 | ||
|
|
b863345600 | ||
|
|
61b49a9339 | ||
|
|
41c80cf3f2 | ||
|
|
6fd3645360 | ||
|
|
b812d0a7f7 | ||
|
|
e8a4c9237d | ||
|
|
5256013615 | ||
|
|
9826c28581 | ||
|
|
9ceceebdc3 | ||
|
|
187d913f84 | ||
|
|
4a0b180d86 | ||
|
|
6fa6055912 | ||
|
|
667995cde4 | ||
|
|
1e0def87fd | ||
|
|
a219e5ee8c | ||
|
|
8070dfcc82 | ||
|
|
176a89bd03 | ||
|
|
dd8dd2f95c | ||
|
|
eb1bd1d200 | ||
|
|
460d2f3c2a | ||
|
|
0ab10f99fd | ||
|
|
377f57f1c3 | ||
|
|
caf5f1f619 | ||
|
|
d9ff85a202 | ||
|
|
f180a7c024 | ||
|
|
7fac9332e1 | ||
|
|
8dd7c42f69 | ||
|
|
b542eeecc0 | ||
|
|
bee8295daa | ||
|
|
ab9fd3d5ed | ||
|
|
cc54a3feda | ||
|
|
94984f35ec | ||
|
|
0e47e89d63 | ||
|
|
2d7dc7a1be | ||
|
|
4d76a5b599 | ||
|
|
87035ff744 | ||
|
|
e0d123f732 | ||
|
|
8eddcfc9d2 | ||
|
|
af58b86b60 | ||
|
|
00ae7ce33c | ||
|
|
0bc9fe841a | ||
|
|
e37920ed4e | ||
|
|
6a7466df93 | ||
|
|
1bb966e5a8 | ||
|
|
34e631395f | ||
|
|
080ddde68d | ||
|
|
209a8026fb | ||
|
|
23bfa4fc43 | ||
|
|
58d40c118c | ||
|
|
9d39769445 | ||
|
|
bfc08abe88 | ||
|
|
6a7b097273 | ||
|
|
8f2390ac99 | ||
|
|
481f5cae22 | ||
|
|
b9068b95b8 | ||
|
|
df2c35b551 | ||
|
|
3cd4152a3c | ||
|
|
2534510f0b | ||
|
|
3f8aa4635e | ||
|
|
ada59e8205 | ||
|
|
9ec0332483 | ||
|
|
d509b0cf5c | ||
|
|
4d624d8c3a | ||
|
|
9f0ba4b9c2 | ||
|
|
a930ae27be | ||
|
|
38e4919be1 | ||
|
|
a668047f75 | ||
|
|
c2ea2cda4c | ||
|
|
f3c3a2c301 | ||
|
|
0da7e587a7 | ||
|
|
e6e686aaf4 | ||
|
|
58e1fa5c36 | ||
|
|
42549526c7 | ||
|
|
9fe1c8fe80 | ||
|
|
b8dbcb3dbd | ||
|
|
7c5675670a | ||
|
|
291945a4fd | ||
|
|
439e8827bd | ||
|
|
a745cf78ee | ||
|
|
af69756df0 | ||
|
|
46c42ab6e4 | ||
|
|
33a127187b | ||
|
|
24ddbdd251 | ||
|
|
0122a98eea | ||
|
|
406545c1f1 | ||
|
|
a1b593027b | ||
|
|
eae1ba258a | ||
|
|
d2db30eabc | ||
|
|
9fb7c52217 | ||
|
|
6cab1786d3 | ||
|
|
362328167c | ||
|
|
570a9993f7 | ||
|
|
5adc68cf0b | ||
|
|
1b1757ebf2 | ||
|
|
d8950fb7d1 | ||
|
|
ba2e573c23 | ||
|
|
31391fc074 | ||
|
|
f94b2c3794 | ||
|
|
eb0a5fed8e | ||
|
|
eaa47d175f | ||
|
|
e968000a89 | ||
|
|
1ba448fe19 | ||
|
|
a5c82425f4 | ||
|
|
1bd31f6b8e | ||
|
|
c0ea0e52b3 | ||
|
|
e6a3daacb3 | ||
|
|
09dabda4a3 | ||
|
|
f523d912af | ||
|
|
90b0ca79ea | ||
|
|
a506e2d5a2 | ||
|
|
4c66518a68 | ||
|
|
42b4b83f8e | ||
|
|
7477ebbdd7 | ||
|
|
738dc5ce19 | ||
|
|
3680467e14 | ||
|
|
c5ada9b203 | ||
|
|
3d2805bc78 | ||
|
|
2dde286d68 | ||
|
|
2260156c40 | ||
|
|
129e970727 | ||
|
|
66271db8c0 | ||
|
|
09d33e62bd | ||
|
|
bf3dfa4ab6 | ||
|
|
40b866117e | ||
|
|
cb5f9f3051 | ||
|
|
80f97cf9bd | ||
|
|
6d860f7eae | ||
|
|
545643b610 | ||
|
|
7ee6f2c36a | ||
|
|
5d9b887624 | ||
|
|
12c0e298f5 | ||
|
|
f9aec7af0d | ||
|
|
b181d78dd5 | ||
|
|
b9ff40c6b5 | ||
|
|
0684810d38 | ||
|
|
1cc7ce6e27 | ||
|
|
82bc1bf0b1 | ||
|
|
75bcf8660b | ||
|
|
5e1d945198 | ||
|
|
e047184ede | ||
|
|
307a2eb6ec | ||
|
|
ab8aedf06e | ||
|
|
b6ab13f1de | ||
|
|
53a3e51920 | ||
|
|
4033566b4a | ||
|
|
bed1623dcb | ||
|
|
d4704977bc | ||
|
|
838eed94bc | ||
|
|
9870725d1f | ||
|
|
ba827283be | ||
|
|
1e37cb8c3c | ||
|
|
1991e01641 | ||
|
|
d7e87b6336 | ||
|
|
fde490ba15 | ||
|
|
cf5a16d967 | ||
|
|
e8dde9c63d | ||
|
|
667a935665 | ||
|
|
28cea706fa | ||
|
|
209a990444 | ||
|
|
6365a46fac | ||
|
|
a81496e9ab | ||
|
|
ca05733b9d | ||
|
|
dfb5348a78 | ||
|
|
602e52490c | ||
|
|
740b24e8a4 | ||
|
|
44a09ffd12 | ||
|
|
054c42cbc2 | ||
|
|
34263a70e2 | ||
|
|
7ea6ca35d7 | ||
|
|
a9aad497fc | ||
|
|
7da8489635 | ||
|
|
683561374d | ||
|
|
66c9982822 | ||
|
|
1b6450b210 | ||
|
|
aa8a13adb2 | ||
|
|
5888541c05 | ||
|
|
f893487dc0 | ||
|
|
b84beaf974 | ||
|
|
75a3c55e70 | ||
|
|
854a09e12f | ||
|
|
40412fd4a9 | ||
|
|
57fc084795 | ||
|
|
143ba6d5e7 | ||
|
|
6b338a923c | ||
|
|
e6ab1e3df5 | ||
|
|
5da6976bf9 | ||
|
|
bd15d90e77 | ||
|
|
61633cf23b | ||
|
|
9f1107c0e7 | ||
|
|
ff0d5ce179 | ||
|
|
0bbd910883 | ||
|
|
4258088fb4 | ||
|
|
6372b677d2 | ||
|
|
9af00af70f | ||
|
|
4010c60e7b | ||
|
|
aaa83a8f52 | ||
|
|
776408c564 | ||
|
|
d0cb2110e6 | ||
|
|
11e3480fe8 | ||
|
|
2cd54b72b0 | ||
|
|
c34ccafb2e | ||
|
|
6837874d43 | ||
|
|
3656337d41 | ||
|
|
a89b6321f1 | ||
|
|
ac10103b18 | ||
|
|
b696a242fc | ||
|
|
7e4822c8ca | ||
|
|
a955cb5400 | ||
|
|
2e2cfc4cb3 | ||
|
|
4157d1986f | ||
|
|
d13eb2f580 | ||
|
|
5476f69179 | ||
|
|
dcdf30da35 | ||
|
|
55746c8c19 | ||
|
|
dbdf5f2746 | ||
|
|
b4e28deed3 | ||
|
|
f4a604dcfb | ||
|
|
b3c5787ec8 | ||
|
|
471d0469dd | ||
|
|
113eda575f | ||
|
|
45f1da82fe | ||
|
|
5f45ff77e4 | ||
|
|
1c0201ee3d | ||
|
|
c7340e04ec | ||
|
|
0a32476dc5 | ||
|
|
e02bc6ffb5 | ||
|
|
f41a3970b2 | ||
|
|
6c536f3a9b | ||
|
|
4b24b6a848 | ||
|
|
5f254a929f | ||
|
|
8df1a01ace | ||
|
|
27b5ffb34f | ||
|
|
80af012962 | ||
|
|
615c80bef4 | ||
|
|
f5f4026dbb | ||
|
|
b431206ede | ||
|
|
c4878e9b49 | ||
|
|
aa452971a6 | ||
|
|
2d798f7cfe | ||
|
|
08bb0484eb | ||
|
|
b0b7337f5a | ||
|
|
93241a4beb | ||
|
|
4f1bf1f13c | ||
|
|
2d0b7b5bd8 | ||
|
|
8fe3ce5cab | ||
|
|
59a0f1d94f | ||
|
|
5175dc3450 | ||
|
|
9a22ccd058 | ||
|
|
c06ed49a2a | ||
|
|
2e51a5a454 | ||
|
|
75cc353528 | ||
|
|
3977580426 | ||
|
|
3a1370e174 | ||
|
|
c218c05b96 | ||
|
|
db247d9f9a | ||
|
|
78b7715ea6 | ||
|
|
ba76944d75 | ||
|
|
4a1a2122f0 | ||
|
|
d80b749dec | ||
|
|
039a8b7c36 | ||
|
|
779f58ab16 | ||
|
|
b9183fe5eb | ||
|
|
9d342671d5 | ||
|
|
4e47ebd5fc | ||
|
|
d5c418e909 | ||
|
|
85414558c5 | ||
|
|
d6af8d2526 | ||
|
|
1209e95e34 | ||
|
|
51f9279e67 | ||
|
|
f27d54f7fa | ||
|
|
7f3648f8ae | ||
|
|
49fc258578 | ||
|
|
0c51b4fe41 | ||
|
|
dbad714539 | ||
|
|
edd8008650 | ||
|
|
615a1b3f4e | ||
|
|
fe6044e1aa | ||
|
|
46b275bfab | ||
|
|
25f44c517a | ||
|
|
cac04f8ee4 | ||
|
|
45d8566ec0 | ||
|
|
29a98ba13b |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -7,6 +7,8 @@ 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>
|
||||
@@ -14,3 +16,5 @@ 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.88.0
|
||||
RUST_VERSION: 1.90.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@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
name: cargo deny
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -67,10 +67,12 @@ jobs:
|
||||
name: Check provider database
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
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
|
||||
|
||||
@@ -80,7 +82,7 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -115,7 +117,7 @@ jobs:
|
||||
shell: bash
|
||||
if: matrix.rust == 'latest'
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -137,12 +139,12 @@ jobs:
|
||||
- name: Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo nextest run --workspace
|
||||
run: cargo nextest run --workspace --locked
|
||||
|
||||
- name: Doc-Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo test --workspace --doc
|
||||
run: cargo test --workspace --locked --doc
|
||||
|
||||
- name: Test cargo vendor
|
||||
run: cargo vendor
|
||||
@@ -154,7 +156,7 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -179,7 +181,7 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -201,7 +203,7 @@ jobs:
|
||||
name: Python lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -226,9 +228,9 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
- os: macos-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -244,19 +246,19 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download libdeltachat.a
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -279,11 +281,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
- os: macos-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
- os: windows-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -297,13 +299,13 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -311,7 +313,7 @@ jobs:
|
||||
run: pip install tox
|
||||
|
||||
- name: Download deltachat-rpc-server
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
|
||||
- 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@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
|
||||
- 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@v4
|
||||
- uses: actions/checkout@v5
|
||||
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@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
|
||||
- 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@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
@@ -285,76 +285,76 @@ jobs:
|
||||
# Needed to publish the binaries to the release.
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
@@ -14,12 +14,12 @@ jobs:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v5
|
||||
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@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Add Rust cache
|
||||
|
||||
24
.github/workflows/nix.yml
vendored
24
.github/workflows/nix.yml
vendored
@@ -5,10 +5,12 @@ on:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
- .github/workflows/nix.yml
|
||||
push:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
- .github/workflows/nix.yml
|
||||
branches:
|
||||
- main
|
||||
|
||||
@@ -19,15 +21,12 @@ jobs:
|
||||
name: check flake formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- run: nix fmt
|
||||
|
||||
# Check that formatting does not change anything.
|
||||
- run: git diff --exit-code
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- run: nix fmt flake.nix -- --check
|
||||
|
||||
build:
|
||||
name: nix build
|
||||
@@ -81,11 +80,11 @@ jobs:
|
||||
#- deltachat-rpc-server-x86_64-android
|
||||
#- deltachat-rpc-server-x86-android
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -96,14 +95,15 @@ jobs:
|
||||
matrix:
|
||||
installable:
|
||||
- deltachat-rpc-server
|
||||
- deltachat-rpc-server-x86_64-darwin
|
||||
|
||||
# Fails to bulid
|
||||
# Fails to build
|
||||
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
|
||||
# - deltachat-rpc-server-aarch64-darwin
|
||||
# - deltachat-rpc-server-x86_64-darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- 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@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -31,12 +31,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- 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@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- 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@v4
|
||||
- uses: actions/checkout@v5
|
||||
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@v4
|
||||
uses: actions/setup-node@v5
|
||||
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@v4
|
||||
- uses: actions/checkout@v5
|
||||
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@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d
|
||||
|
||||
- name: Run zizmor
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -36,6 +36,7 @@ deltachat-ffi/xml
|
||||
coverage/
|
||||
.DS_Store
|
||||
.vscode
|
||||
.zed
|
||||
python/accounts.txt
|
||||
python/all-testaccounts.txt
|
||||
tmp/
|
||||
|
||||
483
CHANGELOG.md
483
CHANGELOG.md
@@ -1,5 +1,459 @@
|
||||
# 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
|
||||
@@ -1457,7 +1911,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 cancelled.
|
||||
- Do not emit progress 1000 when configuration is canceled.
|
||||
- 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)).
|
||||
|
||||
@@ -3905,7 +4359,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 cancelled ([#4391](https://github.com/chatmail/core/pull/4391)).
|
||||
- Delete `smtp` rows when message sending is canceled ([#4391](https://github.com/chatmail/core/pull/4391)).
|
||||
|
||||
### Refactor
|
||||
|
||||
@@ -3916,7 +4370,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 cancelled.
|
||||
- Delete `smtp` rows when message sending is canceled.
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -4003,14 +4457,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 cancelled. #4249
|
||||
- `transfer::get_backup` now frees ongoing process when canceled. #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 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
|
||||
- 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
|
||||
|
||||
### Fixes
|
||||
- Do not return media from trashed messages in the "All media" view. #4247
|
||||
@@ -6495,3 +6949,22 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[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 cancelled"
|
||||
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is canceled"
|
||||
- `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`"
|
||||
|
||||
638
Cargo.lock
generated
638
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
42
Cargo.toml
42
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.3.0"
|
||||
version = "2.22.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.85"
|
||||
@@ -44,14 +44,16 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.10.4", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
async_zip = { version = "0.0.18", 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"
|
||||
@@ -59,17 +61,16 @@ 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.14"
|
||||
hyper-util = "0.1.16"
|
||||
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.35", default-features = false }
|
||||
kamadak-exif = "0.6.1"
|
||||
libc = { workspace = true }
|
||||
mail-builder = { version = "0.4.3", default-features = false }
|
||||
mail-builder = { version = "0.4.4", default-features = false }
|
||||
mailparse = { workspace = true }
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.17"
|
||||
@@ -77,18 +78,16 @@ num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.16.0", default-features = false }
|
||||
pgp = { version = "0.17.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.37"
|
||||
quoted_printable = "0.5"
|
||||
quick-xml = { version = "0.38", features = ["escape-html"] }
|
||||
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"] }
|
||||
@@ -101,22 +100,21 @@ strum_macros = "0.27"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.2"
|
||||
thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-io-timeout = "1.2.1"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
astral-tokio-tar = { version = "0.5.6", default-features = false }
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.8"
|
||||
toml = "0.9"
|
||||
tracing = "0.1.41"
|
||||
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.6.0", features = ["async_tokio"] }
|
||||
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
@@ -175,18 +173,18 @@ harness = false
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
async-channel = "2.3.1"
|
||||
async-channel = "2.5.0"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4.41", default-features = false }
|
||||
chrono = { version = "0.4.42", 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.0"
|
||||
futures-lite = "2.6.1"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
mailparse = "0.16.1"
|
||||
nu-ansi-term = "0.46"
|
||||
nu-ansi-term = "0.50"
|
||||
num-traits = "0.2"
|
||||
rand = "0.8"
|
||||
regex = "1.10"
|
||||
@@ -194,10 +192,10 @@ rusqlite = "0.36"
|
||||
sanitize-filename = "0.5"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.20.0"
|
||||
tempfile = "3.23.0"
|
||||
thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.14"
|
||||
tokio-util = "0.7.16"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.6.4"
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -80,18 +80,26 @@ Connect to your mail server (if already configured):
|
||||
> connect
|
||||
```
|
||||
|
||||
Create a contact:
|
||||
Export your public key to a vCard file:
|
||||
|
||||
```
|
||||
> make-vcard my.vcard 1
|
||||
```
|
||||
|
||||
Create contacts by address or vCard file:
|
||||
|
||||
```
|
||||
> addcontact yourfriends@email.org
|
||||
> import-vcard key-contact.vcard
|
||||
```
|
||||
|
||||
List contacts:
|
||||
|
||||
```
|
||||
> listcontacts
|
||||
Contact#Contact#11: key-contact@email.org <key-contact@email.org>
|
||||
Contact#Contact#Self: Me √ <your@email.org>
|
||||
1 key contacts.
|
||||
2 key contacts.
|
||||
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
|
||||
1 address contacts.
|
||||
```
|
||||
|
||||
33
STYLE.md
33
STYLE.md
@@ -78,6 +78,27 @@ 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`
|
||||
@@ -91,6 +112,18 @@ 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.
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -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 {:?} must not contain whitespaces, '>' or '<'", input);
|
||||
bail!("Email {input:?} must not contain whitespaces, '>' or '<'");
|
||||
}
|
||||
|
||||
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 {:?} must contain '@' character", input),
|
||||
_ => bail!("Email {input:?} must contain '@' character"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.3.0"
|
||||
version = "2.22.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -415,7 +415,6 @@ 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
|
||||
@@ -459,12 +458,6 @@ 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,
|
||||
@@ -503,13 +496,6 @@ 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.
|
||||
@@ -583,11 +569,10 @@ 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, DC_QR_LOGIN or DC_QR_WEBRTC_INSTANCE.
|
||||
* QR code is DC_QR_ACCOUNT or DC_QR_LOGIN.
|
||||
*
|
||||
* 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
|
||||
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
|
||||
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
@@ -1060,42 +1045,6 @@ 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.
|
||||
*
|
||||
@@ -1222,6 +1171,117 @@ 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.
|
||||
*
|
||||
@@ -1339,12 +1399,14 @@ 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.
|
||||
* To get the concrete time of the marker, use dc_array_get_timestamp().
|
||||
* The day marker timestamp is the midnight one for the corresponding (following) day in the local timezone.
|
||||
* 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.
|
||||
@@ -1701,9 +1763,7 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @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 protect Deprecated 2025-08-31, ignored.
|
||||
* @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()
|
||||
@@ -2094,9 +2154,19 @@ int dc_may_be_valid_addr (const char* addr);
|
||||
|
||||
|
||||
/**
|
||||
* Check if an e-mail address belongs to a known and unblocked 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 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().
|
||||
*
|
||||
@@ -2118,6 +2188,13 @@ 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
|
||||
@@ -2493,7 +2570,6 @@ 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
|
||||
@@ -2547,10 +2623,6 @@ 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.
|
||||
@@ -3816,27 +3888,12 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Deprecated, always returns 0.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return 1=chat protected, 0=chat is not protected.
|
||||
* @return Always 0.
|
||||
* @deprecated 2025-09-09
|
||||
*/
|
||||
int dc_chat_is_protected (const dc_chat_t* chat);
|
||||
|
||||
@@ -3856,26 +3913,6 @@ int dc_chat_is_protected (const dc_chat_t* chat);
|
||||
int dc_chat_is_encrypted (const dc_chat_t *chat);
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the chat was protected, and then an incoming message broke this protection.
|
||||
*
|
||||
* This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
|
||||
* otherwise it will return false for all chats.
|
||||
*
|
||||
* 1:1 chats are automatically set as protected when a contact is verified.
|
||||
* When a message comes in that is not encrypted / signed correctly,
|
||||
* the chat is automatically set as unprotected again.
|
||||
* dc_chat_is_protection_broken() will return true until dc_accept_chat() is called.
|
||||
*
|
||||
* The UI should let the user confirm that this is OK with a message like
|
||||
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return 1=chat protection broken, 0=otherwise.
|
||||
*/
|
||||
int dc_chat_is_protection_broken (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check if locations are sent to the chat
|
||||
* at the time the object was created using dc_get_chat().
|
||||
@@ -4651,22 +4688,6 @@ 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.
|
||||
@@ -4689,41 +4710,6 @@ char* dc_msg_get_videochat_url (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.
|
||||
*
|
||||
@@ -5267,20 +5253,14 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Check if the contact
|
||||
* can be added to verified chats,
|
||||
* i.e. has a verified key
|
||||
* and Autocrypt key matches the verified key.
|
||||
* can be added to protected chats.
|
||||
*
|
||||
* 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()).
|
||||
* See dc_contact_get_verifier_id() for a guidance how to display these information.
|
||||
*
|
||||
* @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; in the UI typically checkmarks are shown.
|
||||
* 2: SELF and contact have verified their fingerprints in both directions.
|
||||
*/
|
||||
int dc_contact_is_verified (dc_contact_t* contact);
|
||||
|
||||
@@ -5311,16 +5291,22 @@ int dc_contact_is_key_contact (dc_contact_t* contact);
|
||||
/**
|
||||
* Return the contact ID that verified a contact.
|
||||
*
|
||||
* 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.
|
||||
* As verifier may be unknown,
|
||||
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
|
||||
*
|
||||
* 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.
|
||||
* 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
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -5356,11 +5342,9 @@ 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 and DNS lookup.
|
||||
* Create a provider struct for the given e-mail address by local lookup.
|
||||
*
|
||||
* 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.
|
||||
* DNS lookup is not used anymore and this function is deprecated.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param context The context object.
|
||||
@@ -5368,6 +5352,7 @@ 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);
|
||||
|
||||
@@ -5623,14 +5608,21 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
|
||||
|
||||
/**
|
||||
* Message indicating an incoming or outgoing videochat.
|
||||
* The message was created via dc_send_videochat_invitation() on this or a remote device.
|
||||
* Message indicating an incoming or outgoing 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().
|
||||
* 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
|
||||
*/
|
||||
#define DC_MSG_VIDEOCHAT_INVITATION 70
|
||||
#define DC_MSG_CALL 71
|
||||
|
||||
|
||||
/**
|
||||
@@ -6386,7 +6378,6 @@ 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().
|
||||
*
|
||||
@@ -6476,11 +6467,7 @@ 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 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.
|
||||
* @param data2 (int) The progress, always 1000.
|
||||
*/
|
||||
#define DC_EVENT_SECUREJOIN_INVITER_PROGRESS 2060
|
||||
|
||||
@@ -6632,6 +6619,60 @@ 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
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@@ -6915,11 +6956,6 @@ 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().
|
||||
@@ -7052,6 +7088,8 @@ 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"
|
||||
@@ -7114,17 +7152,6 @@ 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.
|
||||
@@ -7423,7 +7450,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in status messages.
|
||||
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
|
||||
|
||||
/// "You left."
|
||||
/// "You left the group."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_GROUP_LEFT_BY_YOU 132
|
||||
@@ -7594,6 +7621,18 @@ 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.
|
||||
@@ -7670,8 +7709,32 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
|
||||
#define DC_STR_DONATION_REQUEST 193
|
||||
|
||||
/// "Contact". Deprecated, currently unused.
|
||||
#define DC_STR_CONTACT 200
|
||||
/// "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
|
||||
|
||||
/**
|
||||
* @}
|
||||
|
||||
@@ -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, ProtectionStatus};
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, ContactId, Origin};
|
||||
use deltachat::context::{Context, ContextBuilder};
|
||||
@@ -375,7 +375,7 @@ pub unsafe extern "C" fn dc_get_connectivity(context: *const dc_context_t) -> li
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(ctx.get_connectivity()) as u32 as libc::c_int
|
||||
ctx.get_connectivity() as u32 as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -556,6 +556,10 @@ 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"),
|
||||
@@ -619,7 +623,11 @@ 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, .. } => msg_id.to_u32() as libc::c_int,
|
||||
| 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::ChatlistItemChanged { chat_id } => {
|
||||
chat_id.unwrap_or_default().to_u32() as libc::c_int
|
||||
}
|
||||
@@ -671,6 +679,9 @@ 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, .. }
|
||||
@@ -689,6 +700,8 @@ 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"),
|
||||
@@ -767,8 +780,21 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::AccountsChanged
|
||||
| EventType::AccountsItemChanged
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
| 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::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
comment.to_c_string().unwrap_or_default().into_raw()
|
||||
@@ -1072,25 +1098,6 @@ 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,
|
||||
@@ -1167,6 +1174,61 @@ 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,
|
||||
@@ -1659,7 +1721,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() {
|
||||
@@ -1667,22 +1729,12 @@ pub unsafe extern "C" fn dc_create_group_chat(
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let Some(protect) = ProtectionStatus::from_i32(protect)
|
||||
.context("Bad protect-value for dc_create_group_chat()")
|
||||
.log_err(ctx)
|
||||
.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)
|
||||
})
|
||||
block_on(chat::create_group(ctx, &to_string_lossy(name)))
|
||||
.context("Failed to create group chat")
|
||||
.log_err(ctx)
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3144,13 +3196,8 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
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_protected()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat.chat.is_protected() as libc::c_int
|
||||
pub extern "C" fn dc_chat_is_protected(_chat: *mut dc_chat_t) -> libc::c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3165,16 +3212,6 @@ pub unsafe extern "C" fn dc_chat_is_encrypted(chat: *mut dc_chat_t) -> libc::c_i
|
||||
.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]
|
||||
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
@@ -3783,31 +3820,6 @@ 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() {
|
||||
@@ -4634,13 +4646,9 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
|
||||
|
||||
let ctx = &*context;
|
||||
|
||||
match block_on(provider::get_provider_info_by_addr(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
true,
|
||||
))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
match provider::get_provider_info_by_addr(addr.as_str())
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
@@ -4659,25 +4667,13 @@ 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 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(),
|
||||
match provider::get_provider_info_by_addr(addr.as_str())
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ 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)),
|
||||
@@ -105,7 +104,6 @@ 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,
|
||||
@@ -132,7 +130,6 @@ 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(),
|
||||
@@ -185,9 +182,6 @@ 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.3.0"
|
||||
version = "2.22.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,25 +47,26 @@ 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;
|
||||
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AccountState {
|
||||
@@ -91,7 +92,8 @@ pub struct CommandApi {
|
||||
|
||||
/// Receiver side of the event channel.
|
||||
///
|
||||
/// Events from it can be received by calling `get_next_event` method.
|
||||
/// Events from it can be received by calling
|
||||
/// [`CommandApi::get_next_event`] method.
|
||||
event_emitter: Arc<EventEmitter>,
|
||||
|
||||
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
|
||||
@@ -123,7 +125,7 @@ impl CommandApi {
|
||||
.read()
|
||||
.await
|
||||
.get_account(id)
|
||||
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
|
||||
.ok_or_else(|| anyhow!("account with id {id} not found"))?;
|
||||
Ok(sc)
|
||||
}
|
||||
|
||||
@@ -173,7 +175,15 @@ impl CommandApi {
|
||||
get_info()
|
||||
}
|
||||
|
||||
/// Get the next event.
|
||||
/// 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.
|
||||
async fn get_next_event(&self) -> Result<Event> {
|
||||
self.event_emitter
|
||||
.recv()
|
||||
@@ -297,8 +307,7 @@ impl CommandApi {
|
||||
Ok(Account::from_context(&ctx, account_id).await?)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"account with id {} doesn't exist anymore",
|
||||
account_id
|
||||
"account with id {account_id} doesn't exist anymore"
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -326,21 +335,10 @@ impl CommandApi {
|
||||
/// instead of the domain.
|
||||
async fn get_provider_info(
|
||||
&self,
|
||||
account_id: u32,
|
||||
_account_id: u32,
|
||||
email: String,
|
||||
) -> Result<Option<ProviderInfo>> {
|
||||
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;
|
||||
let provider_info = get_provider_info(email.split('@').next_back().unwrap_or(""));
|
||||
Ok(ProviderInfo::from_dc_type(provider_info))
|
||||
}
|
||||
|
||||
@@ -383,11 +381,6 @@ 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?;
|
||||
@@ -886,6 +879,38 @@ 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
|
||||
@@ -969,17 +994,16 @@ 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.
|
||||
///
|
||||
/// @param protect If set to 1 the function creates group with protection initially enabled.
|
||||
/// Only verified members are allowed in these groups
|
||||
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
|
||||
/// `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> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let protect = match protect {
|
||||
true => ProtectionStatus::Protected,
|
||||
false => ProtectionStatus::Unprotected,
|
||||
};
|
||||
chat::create_group_ex(&ctx, Some(protect), &name)
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
chat::create_group(&ctx, &name).await.map(|id| id.to_u32())
|
||||
}
|
||||
|
||||
/// Create a new unencrypted group chat.
|
||||
@@ -988,7 +1012,7 @@ impl CommandApi {
|
||||
/// 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_ex(&ctx, None, &name)
|
||||
chat::create_group_unencrypted(&ctx, &name)
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
}
|
||||
@@ -1060,7 +1084,7 @@ impl CommandApi {
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
visibility: JSONRPCChatVisibility,
|
||||
visibility: JsonrpcChatVisibility,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
@@ -1227,8 +1251,10 @@ impl CommandApi {
|
||||
}
|
||||
|
||||
/// Returns all messages of a particular chat.
|
||||
/// If `add_daymarker` is `true`, it will return them as
|
||||
/// `DC_MSG_ID_DAYMARKER`, e.g. [1234, 1237, 9, 1239].
|
||||
///
|
||||
/// * `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.
|
||||
async fn get_message_ids(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1263,7 +1289,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,
|
||||
@@ -1277,7 +1303,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> {
|
||||
@@ -1471,7 +1497,14 @@ impl CommandApi {
|
||||
|
||||
/// Add a single contact as a result of an explicit user action.
|
||||
///
|
||||
/// Returns contact id of the created or existing contact
|
||||
/// 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.
|
||||
async fn create_contact(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1621,9 +1654,19 @@ impl CommandApi {
|
||||
Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await
|
||||
}
|
||||
|
||||
/// Check if an e-mail address belongs to a known and unblocked 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().
|
||||
async fn lookup_contact_id_by_addr(
|
||||
@@ -1779,13 +1822,13 @@ impl CommandApi {
|
||||
|
||||
/// Offers a backup for remote devices to retrieve.
|
||||
///
|
||||
/// Can be cancelled by stopping the ongoing process. Success or failure can be tracked
|
||||
/// Can be canceled 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 cancelled.
|
||||
/// Returns once a remote device has retrieved the backup, or is canceled.
|
||||
async fn provide_backup(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
@@ -1851,7 +1894,7 @@ impl CommandApi {
|
||||
/// This retrieves the backup from a remote device over the network and imports it into
|
||||
/// the current device.
|
||||
///
|
||||
/// Can be cancelled by stopping the ongoing process.
|
||||
/// Can be canceled 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.
|
||||
@@ -1889,7 +1932,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().await as u32)
|
||||
Ok(ctx.get_connectivity() as u32)
|
||||
}
|
||||
|
||||
/// Get an overview of the current connectivity, and possibly more statistics.
|
||||
@@ -1972,6 +2015,11 @@ 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
|
||||
@@ -2049,6 +2097,53 @@ 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.
|
||||
@@ -2129,7 +2224,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() {
|
||||
@@ -2200,13 +2295,6 @@ 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
|
||||
@@ -2237,8 +2325,7 @@ impl CommandApi {
|
||||
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
ensure!(
|
||||
message.get_viewtype() == Viewtype::Sticker,
|
||||
"message {} is not a sticker",
|
||||
msg_id
|
||||
"message {msg_id} is not a sticker"
|
||||
);
|
||||
let account_folder = ctx
|
||||
.get_dbfile()
|
||||
@@ -2458,10 +2545,7 @@ impl CommandApi {
|
||||
.to_u32();
|
||||
Ok(msg_id)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"chat with id {} doesn't have draft message",
|
||||
chat_id
|
||||
))
|
||||
Err(anyhow!("chat with id {chat_id} doesn't have draft message"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
97
deltachat-jsonrpc/src/api/types/calls.rs
Normal file
97
deltachat-jsonrpc/src/api/types/calls.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
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,17 +19,6 @@ pub struct FullChat {
|
||||
id: u32,
|
||||
name: String,
|
||||
|
||||
/// True if the chat is protected.
|
||||
///
|
||||
/// 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,
|
||||
/// 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",
|
||||
@@ -70,8 +59,15 @@ 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
|
||||
@@ -130,7 +126,6 @@ impl FullChat {
|
||||
Ok(FullChat {
|
||||
id: chat_id,
|
||||
name: chat.name.clone(),
|
||||
is_protected: chat.is_protected(),
|
||||
is_encrypted: chat.is_encrypted(context).await?,
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
@@ -144,7 +139,6 @@ 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(),
|
||||
@@ -172,18 +166,6 @@ pub struct BasicChat {
|
||||
id: u32,
|
||||
name: String,
|
||||
|
||||
/// True if the chat is protected.
|
||||
///
|
||||
/// 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,
|
||||
|
||||
/// 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",
|
||||
@@ -215,7 +197,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,
|
||||
}
|
||||
@@ -234,7 +216,6 @@ impl BasicChat {
|
||||
Ok(BasicChat {
|
||||
id: chat_id,
|
||||
name: chat.name.clone(),
|
||||
is_protected: chat.is_protected(),
|
||||
is_encrypted: chat.is_encrypted(context).await?,
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
@@ -244,7 +225,6 @@ 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(),
|
||||
})
|
||||
@@ -279,18 +259,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, ContactId};
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::{
|
||||
chat::{get_chat_contacts, ChatVisibility},
|
||||
chatlist::Chatlist,
|
||||
@@ -30,7 +30,6 @@ pub enum ChatListItemFetchResult {
|
||||
summary_status: u32,
|
||||
/// showing preview if last chat message is image
|
||||
summary_preview_image: Option<String>,
|
||||
is_protected: bool,
|
||||
|
||||
/// True if the chat is encrypted.
|
||||
/// This means that all messages in the chat are encrypted,
|
||||
@@ -127,11 +126,8 @@ 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)
|
||||
@@ -161,13 +157,12 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
summary_text2,
|
||||
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
|
||||
summary_preview_image,
|
||||
is_protected: chat.is_protected(),
|
||||
is_encrypted: chat.is_encrypted(ctx).await?,
|
||||
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: self_in_group,
|
||||
is_self_in_group: chat.is_self_in_chat(ctx).await?,
|
||||
is_sending_location: chat.is_sending_locations(),
|
||||
is_archived: visibility == ChatVisibility::Archived,
|
||||
is_pinned: visibility == ChatVisibility::Pinned,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -31,27 +31,36 @@ pub struct ContactObject {
|
||||
/// e.g. if we just scanned the fingerprint from a QR code.
|
||||
e2ee_avail: bool,
|
||||
|
||||
/// True if the contact can be added to verified groups.
|
||||
/// True if the contact
|
||||
/// can be added to protected chats
|
||||
/// because SELF and contact have verified their fingerprints in both directions.
|
||||
///
|
||||
/// 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.
|
||||
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
|
||||
is_verified: bool,
|
||||
|
||||
/// True if the contact profile title should have a green checkmark.
|
||||
/// The contact ID that verified a contact.
|
||||
///
|
||||
/// 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.
|
||||
/// As verifier may be unknown,
|
||||
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
|
||||
///
|
||||
/// If this is present,
|
||||
/// display a green checkmark and "Introduced by ..."
|
||||
/// string followed by the verifier contact name and address
|
||||
/// in the contact profile.
|
||||
/// 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.
|
||||
verifier_id: Option<u32>,
|
||||
|
||||
/// the contact's last seen timestamp
|
||||
@@ -72,7 +81,6 @@ 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)
|
||||
@@ -94,7 +102,6 @@ impl ContactObject {
|
||||
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(),
|
||||
@@ -123,7 +130,13 @@ 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 color = color::str_to_color(&vc.addr.to_lowercase());
|
||||
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);
|
||||
Self {
|
||||
addr: vc.addr,
|
||||
display_name,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use deltachat::{Event as CoreEvent, EventType as CoreEventType};
|
||||
use num_traits::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
@@ -224,7 +225,6 @@ 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 information of a secure-join handshake from the view of the inviter
|
||||
/// (Alice, the person who shows the QR code).
|
||||
/// Progress event sent when SecureJoin protocol has finished
|
||||
/// 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,11 +304,14 @@ pub enum EventType {
|
||||
/// ID of the contact that wants to join.
|
||||
contact_id: u32,
|
||||
|
||||
/// 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.
|
||||
/// 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: usize,
|
||||
},
|
||||
|
||||
@@ -417,6 +420,45 @@ 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 {
|
||||
@@ -523,9 +565,13 @@ 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 {
|
||||
@@ -567,6 +613,34 @@ 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")]
|
||||
@@ -84,9 +84,6 @@ pub struct MessageObject {
|
||||
dimensions_height: i32,
|
||||
dimensions_width: i32,
|
||||
|
||||
videochat_type: Option<u32>,
|
||||
videochat_url: Option<String>,
|
||||
|
||||
override_sender_name: Option<String>,
|
||||
sender: ContactObject,
|
||||
|
||||
@@ -105,7 +102,7 @@ pub struct MessageObject {
|
||||
|
||||
saved_message_id: Option<u32>,
|
||||
|
||||
reactions: Option<JSONRPCReactions>,
|
||||
reactions: Option<JsonrpcReactions>,
|
||||
|
||||
vcard_contact: Option<VcardContact>,
|
||||
}
|
||||
@@ -239,15 +236,6 @@ 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,
|
||||
|
||||
@@ -321,8 +309,8 @@ pub enum MessageViewtype {
|
||||
/// Message containing any file, eg. a PDF.
|
||||
File,
|
||||
|
||||
/// Message is an invitation to a videochat.
|
||||
VideochatInvitation,
|
||||
/// Message is a call.
|
||||
Call,
|
||||
|
||||
/// Message is an webxdc instance.
|
||||
Webxdc,
|
||||
@@ -345,7 +333,7 @@ impl From<Viewtype> for MessageViewtype {
|
||||
Viewtype::Voice => MessageViewtype::Voice,
|
||||
Viewtype::Video => MessageViewtype::Video,
|
||||
Viewtype::File => MessageViewtype::File,
|
||||
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
|
||||
Viewtype::Call => MessageViewtype::Call,
|
||||
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
||||
Viewtype::Vcard => MessageViewtype::Vcard,
|
||||
}
|
||||
@@ -364,7 +352,7 @@ impl From<MessageViewtype> for Viewtype {
|
||||
MessageViewtype::Voice => Viewtype::Voice,
|
||||
MessageViewtype::Video => Viewtype::Video,
|
||||
MessageViewtype::File => Viewtype::File,
|
||||
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
|
||||
MessageViewtype::Call => Viewtype::Call,
|
||||
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
||||
MessageViewtype::Vcard => Viewtype::Vcard,
|
||||
}
|
||||
@@ -437,6 +425,9 @@ pub enum SystemMessageType {
|
||||
|
||||
/// This message contains a users iroh node address.
|
||||
IrohNodeAddr,
|
||||
|
||||
CallAccepted,
|
||||
CallEnded,
|
||||
}
|
||||
|
||||
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
@@ -463,6 +454,8 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,7 +532,6 @@ 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,
|
||||
@@ -579,7 +571,6 @@ 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(),
|
||||
@@ -590,7 +581,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,
|
||||
},
|
||||
@@ -603,13 +594,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,4 +1,5 @@
|
||||
pub mod account;
|
||||
pub mod calls;
|
||||
pub mod chat;
|
||||
pub mod chat_list;
|
||||
pub mod contact;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use deltachat::qr::Qr;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
@@ -225,13 +226,6 @@ 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();
|
||||
@@ -311,3 +305,53 @@ 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.3.0"
|
||||
"version": "2.22.0"
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ export class BaseDeltaChat<
|
||||
Transport extends BaseTransport<any>,
|
||||
> extends TinyEmitter<Events> {
|
||||
rpc: RawClient;
|
||||
account?: T.Account;
|
||||
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
||||
|
||||
//@ts-ignore
|
||||
@@ -36,6 +35,10 @@ 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();
|
||||
@@ -45,6 +48,9 @@ export class BaseDeltaChat<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see the constructor's `startEventLoop`
|
||||
*/
|
||||
async eventLoop(): Promise<void> {
|
||||
while (true) {
|
||||
const event = await this.rpc.getNextEvent();
|
||||
@@ -63,10 +69,17 @@ 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];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.3.0"
|
||||
version = "2.22.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -6,9 +6,7 @@ use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use deltachat::chat::{
|
||||
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
|
||||
};
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::*;
|
||||
@@ -210,13 +208,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
} else {
|
||||
""
|
||||
},
|
||||
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 {
|
||||
if msg.get_viewtype() == Viewtype::Webxdc {
|
||||
match msg.get_webxdc_info(context).await {
|
||||
Ok(info) => format!(
|
||||
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
|
||||
@@ -353,7 +345,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
createchat <contact-id>\n\
|
||||
creategroup <name>\n\
|
||||
createbroadcast <name>\n\
|
||||
createprotected <name>\n\
|
||||
addmember <contact-id>\n\
|
||||
removemember <contact-id>\n\
|
||||
groupname <name>\n\
|
||||
@@ -364,6 +355,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
dellocations\n\
|
||||
getlocations [<contact-id>]\n\
|
||||
send <text>\n\
|
||||
send-sync <text>\n\
|
||||
sendempty\n\
|
||||
sendimage <file> [<text>]\n\
|
||||
sendsticker <file> [<text>]\n\
|
||||
@@ -371,7 +363,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
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\
|
||||
@@ -403,6 +394,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
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\
|
||||
@@ -423,7 +416,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.");
|
||||
@@ -437,7 +430,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
setupcodebegin.unwrap_or_default(),
|
||||
);
|
||||
} else {
|
||||
bail!("{} is no setup message.", msg_id,);
|
||||
bail!("{msg_id} is no setup message.",);
|
||||
}
|
||||
}
|
||||
"continue-key-transfer" => {
|
||||
@@ -532,7 +525,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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -567,7 +560,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(),
|
||||
@@ -578,7 +571,6 @@ 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 {
|
||||
@@ -693,7 +685,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(),
|
||||
@@ -711,11 +703,6 @@ 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? {
|
||||
@@ -744,8 +731,7 @@ 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_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
|
||||
let chat_id = chat::create_group(&context, arg1).await?;
|
||||
|
||||
println!("Group#{chat_id} created successfully.");
|
||||
}
|
||||
@@ -755,13 +741,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
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.");
|
||||
@@ -913,6 +892,23 @@ 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?;
|
||||
@@ -960,10 +956,6 @@ 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.");
|
||||
|
||||
@@ -1218,6 +1210,24 @@ 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?;
|
||||
@@ -1239,10 +1249,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"providerinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
||||
let proxy_enabled = context
|
||||
.get_config_bool(config::Config::ProxyEnabled)
|
||||
.await?;
|
||||
match provider::get_provider_info(&context, arg1, proxy_enabled).await {
|
||||
match provider::get_provider_info(arg1) {
|
||||
Some(info) => {
|
||||
println!("Information for provider belonging to {arg1}:");
|
||||
println!("status: {}", info.status as u32);
|
||||
@@ -1278,7 +1285,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
}
|
||||
"" => (),
|
||||
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
|
||||
_ => bail!("Unknown command: \"{arg0}\" type ? for help."),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -199,6 +199,7 @@ const CHAT_COMMANDS: [&str; 39] = [
|
||||
"dellocations",
|
||||
"getlocations",
|
||||
"send",
|
||||
"send-sync",
|
||||
"sendempty",
|
||||
"sendimage",
|
||||
"sendsticker",
|
||||
@@ -206,7 +207,6 @@ const CHAT_COMMANDS: [&str; 39] = [
|
||||
"sendhtml",
|
||||
"sendsyncmsg",
|
||||
"sendupdate",
|
||||
"videochat",
|
||||
"draft",
|
||||
"devicemsg",
|
||||
"listmedia",
|
||||
@@ -232,7 +232,7 @@ const MESSAGE_COMMANDS: [&str; 10] = [
|
||||
"delmsg",
|
||||
"react",
|
||||
];
|
||||
const CONTACT_COMMANDS: [&str; 7] = [
|
||||
const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"listcontacts",
|
||||
"addcontact",
|
||||
"contactinfo",
|
||||
@@ -240,6 +240,8 @@ const CONTACT_COMMANDS: [&str; 7] = [
|
||||
"block",
|
||||
"unblock",
|
||||
"listblocked",
|
||||
"import-vcard",
|
||||
"make-vcard",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 14] = [
|
||||
"getqr",
|
||||
@@ -465,7 +467,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.3.0"
|
||||
version = "2.22.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -19,6 +19,7 @@ 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,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from warnings import warn
|
||||
@@ -185,7 +186,21 @@ class Account:
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||
"""Check if an e-mail address belongs to a known and unblocked 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()."""
|
||||
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||
return contact_id and Contact(self, contact_id)
|
||||
|
||||
@@ -285,7 +300,7 @@ class Account:
|
||||
chats.append(AttrDict(item))
|
||||
return chats
|
||||
|
||||
def create_group(self, name: str, protect: bool = False) -> Chat:
|
||||
def create_group(self, name: str) -> Chat:
|
||||
"""Create a new group chat.
|
||||
|
||||
After creation,
|
||||
@@ -302,12 +317,8 @@ class Account:
|
||||
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.
|
||||
|
||||
: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.
|
||||
"""
|
||||
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
||||
return Chat(self, self._rpc.create_group_chat(self.id, name, False))
|
||||
|
||||
def create_broadcast(self, name: str) -> Chat:
|
||||
"""Create a new **broadcast channel**
|
||||
@@ -456,3 +467,8 @@ 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,6 +168,11 @@ 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]
|
||||
@@ -289,3 +294,8 @@ 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,11 +83,10 @@ 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)
|
||||
self.account.configure()
|
||||
params = {"addr": email, "password": password}
|
||||
self.account.add_or_update_transport(params)
|
||||
self.logger.debug("Account configured")
|
||||
|
||||
def run_forever(self) -> None:
|
||||
|
||||
@@ -73,6 +73,10 @@ 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"
|
||||
@@ -156,7 +160,6 @@ class ViewType(str, Enum):
|
||||
VOICE = "Voice"
|
||||
VIDEO = "Video"
|
||||
FILE = "File"
|
||||
VIDEOCHAT_INVITATION = "VideochatInvitation"
|
||||
WEBXDC = "Webxdc"
|
||||
VCARD = "Vcard"
|
||||
|
||||
@@ -275,11 +278,3 @@ class SocketSecurity(IntEnum):
|
||||
SSL = 1
|
||||
STARTTLS = 2
|
||||
PLAIN = 3
|
||||
|
||||
|
||||
class VideochatType(IntEnum):
|
||||
"""Video chat URL type."""
|
||||
|
||||
UNKNOWN = 0
|
||||
BASICWEBRTC = 1
|
||||
JITSI = 2
|
||||
|
||||
@@ -102,3 +102,15 @@ 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))
|
||||
|
||||
@@ -28,9 +28,7 @@ class ACFactory:
|
||||
|
||||
def get_unconfigured_account(self) -> Account:
|
||||
"""Create a new unconfigured account."""
|
||||
account = self.deltachat.add_account()
|
||||
account.set_config("verified_one_on_one_chats", "1")
|
||||
return account
|
||||
return self.deltachat.add_account()
|
||||
|
||||
def get_unconfigured_bot(self) -> Bot:
|
||||
"""Create a new unconfigured bot."""
|
||||
@@ -75,11 +73,11 @@ class ACFactory:
|
||||
def resetup_account(self, ac: Account) -> Account:
|
||||
"""Resetup account from scratch, losing the encryption key."""
|
||||
ac.stop_io()
|
||||
ac_clone = self.get_unconfigured_account()
|
||||
for i in ["addr", "mail_pw"]:
|
||||
ac_clone.set_config(i, ac.get_config(i))
|
||||
transports = ac.list_transports()
|
||||
ac.remove()
|
||||
ac_clone.configure()
|
||||
ac_clone = self.get_unconfigured_account()
|
||||
for transport in transports:
|
||||
ac_clone.add_or_update_transport(transport)
|
||||
return ac_clone
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||
|
||||
109
deltachat-rpc-client/tests/test_calls.py
Normal file
109
deltachat-rpc-client/tests/test_calls.py
Normal file
@@ -0,0 +1,109 @@
|
||||
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,9 +18,7 @@ def test_autocrypt_setup_message_key_transfer(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.set_config("addr", alice1.get_config("addr"))
|
||||
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
||||
alice2.configure()
|
||||
alice2.add_or_update_transport({"addr": alice1.get_config("addr"), "password": alice1.get_config("mail_pw")})
|
||||
alice2.bring_online()
|
||||
|
||||
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
@@ -37,9 +35,7 @@ def test_ac_setup_message_twice(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.set_config("addr", alice1.get_config("addr"))
|
||||
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
||||
alice2.configure()
|
||||
alice2.add_or_update_transport({"addr": alice1.get_config("addr"), "password": alice1.get_config("mail_pw")})
|
||||
alice2.bring_online()
|
||||
|
||||
# Send the first Autocrypt Setup Message and ignore it.
|
||||
|
||||
@@ -58,8 +58,7 @@ def test_qr_setup_contact_svg(acfactory) -> None:
|
||||
assert "Alice" in svg
|
||||
|
||||
|
||||
@pytest.mark.parametrize("protect", [True, False])
|
||||
def test_qr_securejoin(acfactory, protect):
|
||||
def test_qr_securejoin(acfactory):
|
||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||
|
||||
# Setup second device for Alice
|
||||
@@ -67,8 +66,7 @@ def test_qr_securejoin(acfactory, protect):
|
||||
alice2 = alice.clone()
|
||||
|
||||
logging.info("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group", protect=protect)
|
||||
assert alice_chat.get_basic_snapshot().is_protected == protect
|
||||
alice_chat = alice.create_group("Group")
|
||||
|
||||
logging.info("Bob joins the group")
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
@@ -89,7 +87,6 @@ def test_qr_securejoin(acfactory, protect):
|
||||
|
||||
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)
|
||||
@@ -125,8 +122,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("Verified group", protect=True)
|
||||
logging.info("Bob joins verified group")
|
||||
alice_chat = alice.create_group("Group")
|
||||
logging.info("Bob joins the group")
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
while True:
|
||||
@@ -150,8 +147,8 @@ def test_qr_readreceipt(acfactory) -> None:
|
||||
for joiner in [bob, charlie]:
|
||||
joiner.wait_for_securejoin_joiner_success()
|
||||
|
||||
logging.info("Alice creates a verified group")
|
||||
group = alice.create_group("Group", protect=True)
|
||||
logging.info("Alice creates a group")
|
||||
group = alice.create_group("Group")
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
|
||||
@@ -216,11 +213,10 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
"""Tests verified group recovery by reverifying then 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("ac1 creates a group")
|
||||
chat = ac1.create_group("Group")
|
||||
|
||||
logging.info("ac2 joins verified group")
|
||||
logging.info("ac2 joins the group")
|
||||
qr_code = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
@@ -302,8 +298,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 verified group that ac2 fully joins")
|
||||
ch1 = ac1.create_group("Group", protect=True)
|
||||
logging.info("ac1: create a group that ac2 fully joins")
|
||||
ch1 = ac1.create_group("Group")
|
||||
qr_code = ch1.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
@@ -313,7 +309,6 @@ 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")
|
||||
@@ -327,7 +322,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
assert ac2.create_contact(ac3).get_snapshot().is_verified
|
||||
|
||||
logging.info("ac3: create a verified group VG with ac2")
|
||||
vg = ac3.create_group("ac3-created", protect=True)
|
||||
vg = ac3.create_group("ac3-created")
|
||||
vg.add_contact(ac3.create_contact(ac2))
|
||||
|
||||
# ensure ac2 receives message in VG
|
||||
@@ -335,7 +330,6 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
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")
|
||||
@@ -359,7 +353,7 @@ def test_qr_new_group_unblocked(acfactory):
|
||||
"""
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_chat = ac1.create_group("Group for joining", protect=True)
|
||||
ac1_chat = ac1.create_group("Group for joining")
|
||||
qr_code = ac1_chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
|
||||
@@ -384,8 +378,7 @@ 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", protect=True)
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
chat = ac1.create_group("hello")
|
||||
qr_code = chat.get_qr_code()
|
||||
logging.info("ac2: start QR-code based join-group protocol")
|
||||
ac2.secure_join(qr_code)
|
||||
@@ -439,7 +432,6 @@ 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")
|
||||
@@ -448,13 +440,12 @@ def test_gossip_verification(acfactory) -> None:
|
||||
assert snapshot.text == "Hello Autocrypt group"
|
||||
assert snapshot.show_padlock
|
||||
|
||||
# Autocrypt group does not propagate verification.
|
||||
# Group propagates verification using Autocrypt-Gossip header.
|
||||
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||
assert not carol_contact_alice_snapshot.is_verified
|
||||
assert carol_contact_alice_snapshot.is_verified
|
||||
|
||||
logging.info("Bob creates a Securejoin group")
|
||||
bob_group_chat = bob.create_group("Securejoin Group", protect=True)
|
||||
assert bob_group_chat.get_basic_snapshot().is_protected
|
||||
bob_group_chat = bob.create_group("Securejoin Group")
|
||||
bob_group_chat.add_contact(bob_contact_alice)
|
||||
bob_group_chat.add_contact(bob_contact_carol)
|
||||
bob_group_chat.send_message(text="Hello Securejoin group")
|
||||
@@ -477,7 +468,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("Verified group", protect=True)
|
||||
ac3_chat = ac3.create_group("Group")
|
||||
|
||||
# ac1 joins ac3 group.
|
||||
ac3_qr_code = ac3_chat.get_qr_code()
|
||||
@@ -525,7 +516,6 @@ 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.
|
||||
@@ -535,9 +525,8 @@ 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 verified group")
|
||||
alice_chat = alice.create_group("Verified group", protect=True)
|
||||
assert alice_chat.get_basic_snapshot().is_protected
|
||||
logging.info("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group")
|
||||
logging.info("Bob joins verified group")
|
||||
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
@@ -548,7 +537,6 @@ 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()
|
||||
|
||||
@@ -171,7 +171,10 @@ def test_account(acfactory) -> None:
|
||||
assert alice.get_size()
|
||||
assert alice.is_configured()
|
||||
assert not alice.get_avatar()
|
||||
assert alice.get_contact_by_addr(bob_addr) is None # There is no address-contact, only key-contact
|
||||
# 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_contacts()
|
||||
assert alice.get_contacts(snapshot=True)
|
||||
assert alice.self_contact
|
||||
@@ -249,6 +252,7 @@ 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()
|
||||
|
||||
@@ -260,6 +264,7 @@ 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")
|
||||
@@ -326,6 +331,52 @@ 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()
|
||||
|
||||
@@ -519,8 +570,11 @@ 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["id"] == "gmail"
|
||||
assert provider_info is None
|
||||
|
||||
# Disable MX record resolution.
|
||||
rpc.set_config(account_id, "proxy_enabled", "1")
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
def test_vcard(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||
|
||||
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)
|
||||
@@ -12,3 +15,12 @@ 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.3.0"
|
||||
version = "2.22.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.3.0"
|
||||
"version": "2.22.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,6 +20,11 @@ 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)]
|
||||
|
||||
@@ -33,14 +33,9 @@ skip = [
|
||||
{ name = "lru", version = "0.12.3" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "nom", version = "7.1.3" },
|
||||
{ name = "proc-macro-crate", version = "2.0.0" },
|
||||
{ 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 = "serdect", version = "0.2.0" },
|
||||
{ name = "spin", version = "0.9.8" },
|
||||
@@ -49,7 +44,7 @@ skip = [
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "thiserror-impl", version = "1.0.69" },
|
||||
{ name = "thiserror", version = "1.0.69" },
|
||||
{ name = "toml_edit", version = "0.20.7" },
|
||||
{ name = "toml_datetime", version = "0.6.11" },
|
||||
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
|
||||
{ name = "windows" },
|
||||
{ name = "windows_aarch64_gnullvm" },
|
||||
@@ -67,7 +62,6 @@ skip = [
|
||||
{ name = "windows_x86_64_gnu" },
|
||||
{ name = "windows_x86_64_gnullvm" },
|
||||
{ name = "windows_x86_64_msvc" },
|
||||
{ name = "winnow", version = "0.5.40" },
|
||||
{ name = "zerocopy", version = "0.7.32" },
|
||||
]
|
||||
|
||||
|
||||
13
flake.nix
13
flake.nix
@@ -98,9 +98,6 @@
|
||||
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.
|
||||
};
|
||||
@@ -240,6 +237,9 @@
|
||||
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,12 +483,6 @@
|
||||
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 \
|
||||
@@ -587,6 +581,7 @@
|
||||
(python3.withPackages (pypkgs: with pypkgs; [
|
||||
tox
|
||||
]))
|
||||
nodejs
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
bolero = "0.13.3"
|
||||
bolero = "0.13.4"
|
||||
|
||||
[dependencies]
|
||||
mailparse = { workspace = true }
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.3.0"
|
||||
version = "2.22.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
@@ -330,7 +330,21 @@ class Account:
|
||||
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
||||
|
||||
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
|
||||
"""get a contact for the email address or None if it's blocked or doesn't exist."""
|
||||
"""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()."""
|
||||
_, addr = parseaddr(email)
|
||||
addr = as_dc_charpointer(addr)
|
||||
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
||||
@@ -390,18 +404,16 @@ 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, int(verified), bytes_name)
|
||||
chat_id = lib.dc_create_group_chat(self._dc_context, 0, bytes_name)
|
||||
chat = Chat(self, chat_id)
|
||||
if contacts is not None:
|
||||
for contact in contacts:
|
||||
|
||||
@@ -142,13 +142,6 @@ 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.
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ 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:
|
||||
@@ -164,17 +163,6 @@ 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))
|
||||
@@ -447,10 +435,6 @@ 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
|
||||
@@ -491,7 +475,6 @@ _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,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
"""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))
|
||||
@@ -604,20 +604,6 @@ 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 end-to-end encrypted."
|
||||
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,8 +118,7 @@ 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", verified=True)
|
||||
assert chat1.is_protected()
|
||||
chat1 = ac1.create_group_chat("hello")
|
||||
qr = chat1.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
@@ -142,7 +141,6 @@ 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")
|
||||
@@ -173,8 +171,10 @@ 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
|
||||
ac2_ac3_contact = ac2.get_contacts()[1]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
|
||||
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
|
||||
|
||||
lp.sec("ac2: send message and let ac3 read it")
|
||||
chat2.send_text("hi")
|
||||
@@ -266,8 +266,7 @@ 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", verified=True)
|
||||
assert chat.is_protected()
|
||||
chat = ac1.create_group_chat("hello")
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
@@ -321,8 +320,7 @@ 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", verified=True)
|
||||
assert chat.is_protected()
|
||||
chat = ac1.create_group_chat("hello")
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
ac2.qr_join_chat(qr)
|
||||
@@ -336,7 +334,6 @@ 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.is_protected()
|
||||
assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted."
|
||||
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
|
||||
|
||||
@@ -376,8 +373,7 @@ 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", verified=True)
|
||||
assert chat1.is_protected()
|
||||
chat1 = ac1.create_group_chat("hello")
|
||||
qr = chat1.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
@@ -402,28 +398,17 @@ 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 end-to-end encrypted."
|
||||
|
||||
# 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()
|
||||
|
||||
|
||||
|
||||
@@ -160,32 +160,6 @@ 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)
|
||||
@@ -334,7 +308,7 @@ def test_move_works(acfactory):
|
||||
|
||||
|
||||
def test_move_avoids_loop(acfactory):
|
||||
"""Test that the message is only moved once.
|
||||
"""Test that the message is only moved from INBOX to DeltaChat.
|
||||
|
||||
This is to avoid busy loop if moved message reappears in the Inbox
|
||||
or some scanned folder later.
|
||||
@@ -345,6 +319,14 @@ 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")
|
||||
|
||||
@@ -352,20 +334,28 @@ 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 again.
|
||||
# Move the message to the INBOX.DeltaChat again.
|
||||
# We assume that test server uses "." as the delimiter.
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
ac2.direct_imap.conn.move(["*"], "INBOX")
|
||||
ac2.direct_imap.conn.move(["*"], "INBOX.DeltaChat")
|
||||
|
||||
ac1_chat.send_text("Message 2")
|
||||
ac2_msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_msg2.text == "Message 2"
|
||||
|
||||
# Check that Message 1 is still in the INBOX folder
|
||||
# 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
|
||||
# and Message 2 is in the DeltaChat folder.
|
||||
ac2.direct_imap.select_folder("INBOX")
|
||||
assert len(ac2.direct_imap.get_all_messages()) == 1
|
||||
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")
|
||||
assert len(ac2.direct_imap.get_all_messages()) == 1
|
||||
|
||||
|
||||
def test_move_works_on_self_sent(acfactory):
|
||||
@@ -432,7 +422,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) == 2
|
||||
assert len(messages) == 3
|
||||
msg = messages[-1]
|
||||
assert msg.is_forwarded()
|
||||
ac2.delete_messages(messages)
|
||||
@@ -476,14 +466,19 @@ 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("ac2: check that message is deleted")
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
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())
|
||||
|
||||
assert len(chat2.get_messages()) == chat2_msg_cnt
|
||||
|
||||
|
||||
@@ -1230,7 +1225,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 via gossip in a verified group
|
||||
that resulted in failure to propagate verification
|
||||
when the database already contained the contact with a different email address capitalization.
|
||||
"""
|
||||
|
||||
@@ -1241,17 +1236,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 verified group with a QR code")
|
||||
chat = ac3.create_group_chat("hello", verified=True)
|
||||
lp.sec("ac3 creates a group with a QR code")
|
||||
chat = ac3.create_group_chat("hello")
|
||||
qr = chat.get_join_qr()
|
||||
|
||||
lp.sec("ac1 joins a verified group via a QR code")
|
||||
lp.sec("ac1 joins a 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 verified group via a QR code")
|
||||
lp.sec("ac2 joins a group via a QR code")
|
||||
ac2.qr_join_chat(qr)
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
@@ -1781,12 +1776,12 @@ def test_group_quote(acfactory, lp):
|
||||
"xyz",
|
||||
False,
|
||||
"xyz",
|
||||
), # Test that emails are recognized in a random folder but not moved
|
||||
), # Test that emails aren't found in a random folder
|
||||
(
|
||||
"xyz",
|
||||
True,
|
||||
"DeltaChat",
|
||||
), # ...emails are found in a random folder and moved to DeltaChat
|
||||
"xyz",
|
||||
), # ...emails are found in a random folder and downloaded without moving
|
||||
(
|
||||
"Spam",
|
||||
False,
|
||||
@@ -1811,7 +1806,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 the mvbox")
|
||||
lp.sec("Send a message to from ac2 to ac1 and manually move it to `folder`")
|
||||
ac1.direct_imap.select_config_folder("inbox")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
|
||||
@@ -1821,10 +1816,17 @@ 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()
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
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
|
||||
|
||||
# The message has been downloaded, which means it has reached its destination.
|
||||
# The message has reached its destination.
|
||||
ac1.direct_imap.select_folder(expected_destination)
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 1
|
||||
if folder != expected_destination:
|
||||
|
||||
@@ -271,10 +271,9 @@ class TestOfflineChat:
|
||||
chat.set_name("Homework")
|
||||
assert chat.get_messages()[-1].text == "abc homework xyz Homework"
|
||||
|
||||
@pytest.mark.parametrize("verified", [True, False])
|
||||
def test_group_chat_qr(self, acfactory, ac1, verified):
|
||||
def test_group_chat_qr(self, acfactory, ac1):
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_group_chat(name="title1", verified=verified)
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
assert chat.is_group()
|
||||
qr = chat.get_join_qr()
|
||||
assert ac2.check_qr(qr).is_ask_verifygroup
|
||||
@@ -663,4 +662,4 @@ class TestOfflineChat:
|
||||
|
||||
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) == 3
|
||||
assert len(sysmessages) == 4
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-07-19
|
||||
2025-10-17
|
||||
@@ -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.88.0
|
||||
RUST_VERSION=1.90.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=77cbf92a8565fdf1bcaba10fa93c1455c750a1e9
|
||||
REV=1cce91c1f1065b47e4f307d6fe2f4cca68c74d2e
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
20
spec.md
20
spec.md
@@ -1,6 +1,6 @@
|
||||
# Chatmail Specification
|
||||
|
||||
Version: 0.36.0
|
||||
Version: 0.37.0
|
||||
Status: In-progress
|
||||
Format: [Semantic Line Breaks](https://sembr.org/)
|
||||
|
||||
@@ -582,6 +582,24 @@ 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:
|
||||
|
||||
@@ -78,7 +78,7 @@ impl Accounts {
|
||||
ensure!(dir.exists(), "directory does not exist");
|
||||
|
||||
let config_file = dir.join(CONFIG_NAME);
|
||||
ensure!(config_file.exists(), "{:?} does not exist", config_file);
|
||||
ensure!(config_file.exists(), "{config_file:?} does not exist");
|
||||
|
||||
let config = Config::from_file(config_file, writable).await?;
|
||||
let events = Events::new();
|
||||
@@ -724,8 +724,7 @@ 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;
|
||||
|
||||
@@ -17,7 +17,6 @@ pub enum EncryptPreference {
|
||||
#[default]
|
||||
NoPreference = 0,
|
||||
Mutual = 1,
|
||||
Reset = 20,
|
||||
}
|
||||
|
||||
impl fmt::Display for EncryptPreference {
|
||||
@@ -25,7 +24,6 @@ impl fmt::Display for EncryptPreference {
|
||||
match *self {
|
||||
EncryptPreference::Mutual => write!(fmt, "mutual"),
|
||||
EncryptPreference::NoPreference => write!(fmt, "nopreference"),
|
||||
EncryptPreference::Reset => write!(fmt, "reset"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +35,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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,21 +46,13 @@ pub struct Aheader {
|
||||
pub addr: String,
|
||||
pub public_key: SignedPublicKey,
|
||||
pub prefer_encrypt: EncryptPreference,
|
||||
}
|
||||
|
||||
impl Aheader {
|
||||
/// Creates new autocrypt header
|
||||
pub fn new(
|
||||
addr: String,
|
||||
public_key: SignedPublicKey,
|
||||
prefer_encrypt: EncryptPreference,
|
||||
) -> Self {
|
||||
Aheader {
|
||||
addr,
|
||||
public_key,
|
||||
prefer_encrypt,
|
||||
}
|
||||
}
|
||||
// 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 fmt::Display for Aheader {
|
||||
@@ -71,6 +61,9 @@ 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
|
||||
@@ -125,6 +118,8 @@ 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('_')) {
|
||||
@@ -135,6 +130,7 @@ impl FromStr for Aheader {
|
||||
addr,
|
||||
public_key,
|
||||
prefer_encrypt,
|
||||
verified,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -152,10 +148,11 @@ mod tests {
|
||||
|
||||
assert_eq!(h.addr, "me@mail.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
|
||||
assert_eq!(h.verified, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// EncryptPreference::Reset is an internal value, parser should never return it
|
||||
// Non-standard values of prefer-encrypt such as `reset` are treated as no preference.
|
||||
#[test]
|
||||
fn test_from_str_reset() -> Result<()> {
|
||||
let raw = format!("addr=reset@example.com; prefer-encrypt=reset; keydata={RAWKEY}");
|
||||
@@ -245,11 +242,12 @@ mod tests {
|
||||
assert!(
|
||||
format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"test@example.com".to_string(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::Mutual
|
||||
)
|
||||
Aheader {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.contains("prefer-encrypt=mutual;")
|
||||
);
|
||||
@@ -260,11 +258,12 @@ mod tests {
|
||||
assert!(
|
||||
!format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"test@example.com".to_string(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::NoPreference
|
||||
)
|
||||
Aheader {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.contains("prefer-encrypt")
|
||||
);
|
||||
@@ -273,13 +272,27 @@ mod tests {
|
||||
assert!(
|
||||
format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"TeSt@eXaMpLe.cOm".to_string(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::Mutual
|
||||
)
|
||||
Aheader {
|
||||
addr: "TeSt@eXaMpLe.cOm".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:#}"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
18
src/blob.rs
18
src/blob.rs
@@ -170,7 +170,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(),
|
||||
@@ -367,11 +367,12 @@ impl<'a> BlobObject<'a> {
|
||||
|| 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);
|
||||
}
|
||||
}
|
||||
if *vt == Viewtype::Sticker && exif.is_none() {
|
||||
return Ok(name);
|
||||
}
|
||||
|
||||
img = match orientation {
|
||||
Some(90) => img.rotate90(),
|
||||
@@ -457,8 +458,7 @@ impl<'a> BlobObject<'a> {
|
||||
{
|
||||
if img_wh < 20 {
|
||||
return Err(format_err!(
|
||||
"Failed to scale image to below {}B.",
|
||||
max_bytes,
|
||||
"Failed to scale image to below {max_bytes}B.",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -537,7 +537,11 @@ 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().read_from_container(&mut bufreader).ok();
|
||||
let exif = exif::Reader::new()
|
||||
.continue_on_error(true)
|
||||
.read_from_container(&mut bufreader)
|
||||
.or_else(|e| e.distill_partial_result(|_errors| {}))
|
||||
.ok();
|
||||
Ok((len, exif))
|
||||
}
|
||||
|
||||
|
||||
@@ -334,6 +334,28 @@ 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");
|
||||
@@ -416,6 +438,28 @@ 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)]
|
||||
@@ -485,6 +529,7 @@ 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,
|
||||
@@ -500,6 +545,7 @@ 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;
|
||||
@@ -550,7 +596,7 @@ impl SendImageCheckMediaquality<'_> {
|
||||
}
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
|
||||
assert_eq!(bob_msg.get_viewtype(), res_viewtype);
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = bob
|
||||
@@ -564,7 +610,7 @@ impl SendImageCheckMediaquality<'_> {
|
||||
}
|
||||
|
||||
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||
assert!(exif.is_none());
|
||||
assert!(res_viewtype != Viewtype::Image || exif.is_none());
|
||||
|
||||
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
||||
|
||||
|
||||
702
src/calls.rs
Normal file
702
src/calls.rs
Normal file
@@ -0,0 +1,702 @@
|
||||
//! # 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;
|
||||
674
src/calls/calls_tests.rs
Normal file
674
src/calls/calls_tests.rs
Normal file
@@ -0,0 +1,674 @@
|
||||
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(())
|
||||
}
|
||||
790
src/chat.rs
790
src/chat.rs
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,9 @@ use crate::test_utils::{
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, E2EE_INFO_MSGS, TestContext, TestContextManager,
|
||||
TimeShiftFalsePositiveNote, sync,
|
||||
};
|
||||
use crate::tools::SystemTime;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::time::Duration;
|
||||
use strum::IntoEnumIterator;
|
||||
use tokio::fs;
|
||||
|
||||
@@ -32,7 +34,7 @@ async fn test_chat_info() {
|
||||
"archived": false,
|
||||
"param": "",
|
||||
"is_sending_locations": false,
|
||||
"color": 35391,
|
||||
"color": 29381,
|
||||
"profile_image": {},
|
||||
"draft": "",
|
||||
"is_muted": false,
|
||||
@@ -94,7 +96,7 @@ async fn test_get_draft() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_draft() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
let chat_id = create_group(&t, "abc").await?;
|
||||
|
||||
let mut msg = Message::new_text("hi!".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await?;
|
||||
@@ -118,7 +120,7 @@ async fn test_forwarding_draft_failing() -> Result<()> {
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await?;
|
||||
assert_eq!(msg.id, chat_id.get_draft(&t).await?.unwrap().id);
|
||||
|
||||
let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let chat_id2 = create_group(&t, "foo").await?;
|
||||
assert!(forward_msgs(&t, &[msg.id], chat_id2).await.is_err());
|
||||
Ok(())
|
||||
}
|
||||
@@ -167,7 +169,7 @@ async fn test_draft_stable_ids() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_only_one_draft_per_chat() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
let chat_id = create_group(&t, "abc").await?;
|
||||
|
||||
let msgs: Vec<message::Message> = (1..=1000)
|
||||
.map(|i| Message::new_text(i.to_string()))
|
||||
@@ -194,7 +196,7 @@ async fn test_only_one_draft_per_chat() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_change_quotes_on_reused_message_object() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
|
||||
let chat_id = create_group(&t, "chat").await?;
|
||||
let quote1 =
|
||||
Message::load_from_db(&t, send_text_msg(&t, chat_id, "quote1".to_string()).await?).await?;
|
||||
let quote2 =
|
||||
@@ -245,7 +247,7 @@ async fn test_quote_replies() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let grp_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
let grp_chat_id = create_group(&alice, "grp").await?;
|
||||
let grp_msg_id = send_text_msg(&alice, grp_chat_id, "bar".to_string()).await?;
|
||||
let grp_msg = Message::load_from_db(&alice, grp_msg_id).await?;
|
||||
|
||||
@@ -293,9 +295,7 @@ async fn test_quote_replies() -> Result<()> {
|
||||
async fn test_add_contact_to_chat_ex_add_self() {
|
||||
// Adding self to a contact should succeed, even though it's pointless.
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id = create_group(&t, "foo").await.unwrap();
|
||||
let added = add_contact_to_chat_ex(&t, Nosync, chat_id, ContactId::SELF, false)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -334,8 +334,7 @@ async fn test_member_add_remove() -> Result<()> {
|
||||
}
|
||||
|
||||
tcm.section("Create and promote a group.");
|
||||
let alice_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let alice_chat_id = create_group(&alice, "Group chat").await?;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
let sent = alice
|
||||
@@ -397,8 +396,7 @@ async fn test_parallel_member_remove() -> Result<()> {
|
||||
let alice_charlie_contact_id = alice.add_or_lookup_contact_id(&charlie).await;
|
||||
|
||||
tcm.section("Alice creates and promotes a group");
|
||||
let alice_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let alice_chat_id = create_group(&alice, "Group chat").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
let alice_sent_msg = alice
|
||||
@@ -455,8 +453,7 @@ async fn test_msg_with_implicit_member_removed() -> Result<()> {
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
let bob_fiona_contact_id = bob.add_or_lookup_contact_id(&fiona).await;
|
||||
let alice_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let alice_chat_id = create_group(&alice, "Group chat").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent_msg = alice.send_text(alice_chat_id, "I created a group").await;
|
||||
let bob_received_msg = bob.recv_msg(&sent_msg).await;
|
||||
@@ -486,7 +483,7 @@ async fn test_msg_with_implicit_member_removed() -> Result<()> {
|
||||
// If Bob sends a message to Alice now, Fiona is removed.
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
|
||||
let sent_msg = bob
|
||||
.send_text(alice_chat_id, "I have removed Fiona some time ago.")
|
||||
.send_text(bob_chat_id, "I have removed Fiona some time ago.")
|
||||
.await;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
|
||||
@@ -502,7 +499,7 @@ async fn test_modify_chat_multi_device() -> Result<()> {
|
||||
a1.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
// create group and sync it to the second device
|
||||
let a1_chat_id = create_group_chat(&a1, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let a1_chat_id = create_group(&a1, "foo").await?;
|
||||
let sent = a1.send_text(a1_chat_id, "ho!").await;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
let a1_chat = Chat::load_from_db(&a1, a1_chat_id).await?;
|
||||
@@ -600,7 +597,7 @@ async fn test_modify_chat_disordered() -> Result<()> {
|
||||
let fiona = tcm.fiona().await;
|
||||
let fiona_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let alice_chat_id = create_group(&alice, "foo").await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
@@ -647,9 +644,7 @@ async fn test_lost_member_added() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob])
|
||||
.await;
|
||||
let alice_chat_id = alice.create_group_with_members("Group", &[bob]).await;
|
||||
let alice_sent = alice.send_text(alice_chat_id, "Hi!").await;
|
||||
let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id;
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
|
||||
@@ -679,7 +674,7 @@ async fn test_modify_chat_lost() -> Result<()> {
|
||||
let fiona = tcm.fiona().await;
|
||||
let fiona_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let alice_chat_id = create_group(&alice, "foo").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, charlie_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, fiona_id).await?;
|
||||
@@ -720,7 +715,7 @@ async fn test_leave_group() -> Result<()> {
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
tcm.section("Alice creates group chat with Bob.");
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let alice_chat_id = create_group(&alice, "foo").await?;
|
||||
let bob_contact = alice.add_or_lookup_contact(&bob).await.id;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?;
|
||||
|
||||
@@ -1379,9 +1374,7 @@ async fn test_pinned() {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
|
||||
let chat_id2 = t.get_self_chat().await.id;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
|
||||
let chat_id3 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id3 = create_group(&t, "foo").await.unwrap();
|
||||
|
||||
let chatlist = get_chats_from_chat_list(&t, DC_GCL_NO_SPECIALS).await;
|
||||
assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]);
|
||||
@@ -1471,9 +1464,7 @@ async fn test_set_chat_name() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id = create_group(alice, "foo").await.unwrap();
|
||||
assert_eq!(
|
||||
Chat::load_from_db(alice, chat_id).await.unwrap().get_name(),
|
||||
"foo"
|
||||
@@ -1545,7 +1536,7 @@ async fn test_shall_attach_selfavatar() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let chat_id = create_group(alice, "foo").await?;
|
||||
assert!(!shall_attach_selfavatar(alice, chat_id).await?);
|
||||
|
||||
let contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
@@ -1567,7 +1558,7 @@ async fn test_profile_data_on_group_leave() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let chat_id = create_group(t, "foo").await?;
|
||||
|
||||
let contact_id = t.add_or_lookup_contact_id(bob).await;
|
||||
add_contact_to_chat(t, chat_id, contact_id).await?;
|
||||
@@ -1592,9 +1583,7 @@ async fn test_profile_data_on_group_leave() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_mute_duration() {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id = create_group(&t, "foo").await.unwrap();
|
||||
// Initial
|
||||
assert_eq!(
|
||||
Chat::load_from_db(&t, chat_id).await.unwrap().is_muted(),
|
||||
@@ -1643,8 +1632,8 @@ async fn test_set_mute_duration() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_info_msg() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
add_info_msg(&t, chat_id, "foo info", 200000).await?;
|
||||
let chat_id = create_group(&t, "foo").await?;
|
||||
add_info_msg(&t, chat_id, "foo info", time()).await?;
|
||||
|
||||
let msg = t.get_last_msg_in(chat_id).await;
|
||||
assert_eq!(msg.get_chat_id(), chat_id);
|
||||
@@ -1660,13 +1649,13 @@ async fn test_add_info_msg() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_info_msg_with_cmd() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let chat_id = create_group(&t, "foo").await?;
|
||||
let msg_id = add_info_msg_with_cmd(
|
||||
&t,
|
||||
chat_id,
|
||||
"foo bar info",
|
||||
SystemMessage::EphemeralTimerChanged,
|
||||
10000,
|
||||
time(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -1929,19 +1918,31 @@ async fn test_classic_email_chat() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_chat_get_color() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?;
|
||||
let chat_id = create_group_unencrypted(&t, "a chat").await?;
|
||||
let color1 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?;
|
||||
assert_eq!(color1, 0x008772);
|
||||
assert_eq!(color1, 0x6239dc);
|
||||
|
||||
// upper-/lowercase makes a difference for the colors, these are different groups
|
||||
// (in contrast to email addresses, where upper-/lowercase is ignored in practise)
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "A CHAT").await?;
|
||||
let chat_id = create_group_unencrypted(&t, "A CHAT").await?;
|
||||
let color2 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?;
|
||||
assert_ne!(color2, color1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_chat_get_color_encrypted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let chat_id = create_group(t, "a chat").await?;
|
||||
let color1 = Chat::load_from_db(t, chat_id).await?.get_color(t).await?;
|
||||
set_chat_name(t, chat_id, "A CHAT").await?;
|
||||
let color2 = Chat::load_from_db(t, chat_id).await?.get_color(t).await?;
|
||||
assert_eq!(color2, color1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_sticker(
|
||||
filename: &str,
|
||||
bytes: &[u8],
|
||||
@@ -2123,7 +2124,7 @@ async fn test_forward_info_msg() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let chat_id1 = create_group_chat(alice, ProtectionStatus::Unprotected, "a").await?;
|
||||
let chat_id1 = create_group(alice, "a").await?;
|
||||
send_text_msg(alice, chat_id1, "msg one".to_string()).await?;
|
||||
let bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
add_contact_to_chat(alice, chat_id1, bob_id).await?;
|
||||
@@ -2190,8 +2191,7 @@ async fn test_forward_group() -> Result<()> {
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
|
||||
// Alice creates a group with Bob.
|
||||
let alice_group_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
let alice_group_chat_id = create_group(&alice, "Group").await?;
|
||||
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let charlie_id = alice.add_or_lookup_contact_id(&charlie).await;
|
||||
add_contact_to_chat(&alice, alice_group_chat_id, bob_id).await?;
|
||||
@@ -2243,8 +2243,7 @@ async fn test_only_minimal_data_are_forwarded() -> Result<()> {
|
||||
.set_config(Config::Displayname, Some("secretname"))
|
||||
.await?;
|
||||
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let group_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?;
|
||||
let group_id = create_group(&alice, "secretgrpname").await?;
|
||||
add_contact_to_chat(&alice, group_id, bob_id).await?;
|
||||
let mut msg = Message::new_text("bla foo".to_owned());
|
||||
let sent_msg = alice.send_msg(group_id, &mut msg).await;
|
||||
@@ -2259,7 +2258,7 @@ async fn test_only_minimal_data_are_forwarded() -> Result<()> {
|
||||
let orig_msg = bob.recv_msg(&sent_msg).await;
|
||||
let charlie_id = bob.add_or_lookup_contact_id(&charlie).await;
|
||||
let single_id = ChatId::create_for_contact(&bob, charlie_id).await?;
|
||||
let group_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "group2").await?;
|
||||
let group_id = create_group(&bob, "group2").await?;
|
||||
add_contact_to_chat(&bob, group_id, charlie_id).await?;
|
||||
let broadcast_id = create_broadcast(&bob, "Channel".to_string()).await?;
|
||||
add_contact_to_chat(&bob, broadcast_id, charlie_id).await?;
|
||||
@@ -2280,14 +2279,19 @@ async fn test_only_minimal_data_are_forwarded() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
let sent = alice.send_text(alice_chat.get_id(), "hi, bob").await;
|
||||
let sent_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?;
|
||||
assert!(sent_msg.get_saved_msg_id(&alice).await?.is_none());
|
||||
assert!(sent_msg.get_original_msg_id(&alice).await?.is_none());
|
||||
let sent_timestamp = sent_msg.get_timestamp();
|
||||
assert!(sent_timestamp > 0);
|
||||
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
|
||||
let self_chat = alice.get_self_chat().await;
|
||||
save_msgs(&alice, &[sent.sender_msg_id]).await?;
|
||||
@@ -2305,6 +2309,8 @@ async fn test_save_msgs() -> Result<()> {
|
||||
assert_eq!(saved_msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(saved_msg.get_state(), MessageState::OutDelivered);
|
||||
assert_ne!(saved_msg.rfc724_mid(), sent_msg.rfc724_mid());
|
||||
let saved_timestamp = saved_msg.get_timestamp();
|
||||
assert_eq!(saved_timestamp, sent_timestamp);
|
||||
|
||||
let sent_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?;
|
||||
assert_eq!(
|
||||
@@ -2350,7 +2356,7 @@ async fn test_save_msgs_order() -> Result<()> {
|
||||
for a in [alice, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let chat_id = create_group_chat(alice, ProtectionStatus::Protected, "grp").await?;
|
||||
let chat_id = create_group(alice, "grp").await?;
|
||||
let sent = [
|
||||
alice.send_text(chat_id, "0").await,
|
||||
alice.send_text(chat_id, "1").await,
|
||||
@@ -2406,14 +2412,14 @@ async fn test_forward_from_saved_to_saved() -> Result<()> {
|
||||
let bob = TestContext::new_bob().await;
|
||||
let sent = alice.send_text(alice.create_chat(&bob).await.id, "k").await;
|
||||
|
||||
bob.recv_msg(&sent).await;
|
||||
let received_message = bob.recv_msg(&sent).await;
|
||||
let orig = bob.get_last_msg().await;
|
||||
let self_chat = bob.get_self_chat().await;
|
||||
save_msgs(&bob, &[orig.id]).await?;
|
||||
let saved1 = bob.get_last_msg().await;
|
||||
assert_eq!(
|
||||
saved1.get_original_msg_id(&bob).await?.unwrap(),
|
||||
sent.sender_msg_id
|
||||
received_message.id
|
||||
);
|
||||
assert_ne!(saved1.from_id, ContactId::SELF);
|
||||
|
||||
@@ -2471,7 +2477,7 @@ async fn test_resend_own_message() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let fiona = TestContext::new_fiona().await;
|
||||
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
let alice_grp = create_group(&alice, "grp").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
@@ -2558,7 +2564,7 @@ async fn test_resend_foreign_message_fails() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_grp = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
let alice_grp = create_group(alice, "grp").await?;
|
||||
add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?;
|
||||
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
|
||||
|
||||
@@ -2575,7 +2581,7 @@ async fn test_resend_info_message_fails() -> Result<()> {
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let alice_grp = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
let alice_grp = create_group(alice, "grp").await?;
|
||||
add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?;
|
||||
alice.send_text(alice_grp, "alice->bob").await;
|
||||
|
||||
@@ -2598,7 +2604,7 @@ async fn test_can_send_group() -> Result<()> {
|
||||
let chat_id = ChatId::create_for_contact(&alice, bob).await?;
|
||||
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
||||
assert!(chat.can_send(&alice).await?);
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let chat_id = create_group(&alice, "foo").await?;
|
||||
assert_eq!(
|
||||
Chat::load_from_db(&alice, chat_id)
|
||||
.await?
|
||||
@@ -2638,7 +2644,7 @@ async fn test_broadcast() -> Result<()> {
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
broadcast_id,
|
||||
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
|
||||
get_chat_contacts(&alice, msg.chat_id).await?.pop().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
let fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
@@ -3005,7 +3011,7 @@ async fn test_leave_broadcast() -> Result<()> {
|
||||
}
|
||||
|
||||
/// Tests that if Bob leaves a broadcast channel with one device,
|
||||
/// the other device shows a correct info message "You left.".
|
||||
/// the other device shows a correct info message "You left the channel.".
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -3040,10 +3046,7 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
assert_eq!(rcvd.chat_id, bob1_hello.chat_id);
|
||||
assert!(rcvd.is_info());
|
||||
assert_eq!(rcvd.get_info_type(), SystemMessage::MemberRemovedFromGroup);
|
||||
assert_eq!(
|
||||
rcvd.text,
|
||||
stock_str::msg_group_left_local(bob1, ContactId::SELF).await
|
||||
);
|
||||
assert_eq!(rcvd.text, stock_str::msg_you_left_broadcast(bob1).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3097,7 +3100,7 @@ async fn test_chat_get_encryption_info() -> Result<()> {
|
||||
let contact_bob = alice.add_or_lookup_contact_id(bob).await;
|
||||
let contact_fiona = alice.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
let chat_id = create_group(alice, "Group").await?;
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(alice).await?,
|
||||
"End-to-end encryption available"
|
||||
@@ -3152,11 +3155,33 @@ async fn test_chat_get_encryption_info() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_out_failed_on_all_keys_missing() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let bob_chat_id = bob.create_group_with_members("", &[alice, fiona]).await;
|
||||
bob.send_text(bob_chat_id, "Gossiping Fiona's key").await;
|
||||
alice
|
||||
.recv_msg(&bob.send_text(bob_chat_id, "No key gossip").await)
|
||||
.await;
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
remove_contact_from_chat(bob, bob_chat_id, ContactId::SELF).await?;
|
||||
let alice_chat_id = alice.recv_msg(&bob.pop_sent_msg().await).await.chat_id;
|
||||
alice_chat_id.accept(alice).await?;
|
||||
let mut msg = Message::new_text("Hi".to_string());
|
||||
send_msg(alice, alice_chat_id, &mut msg).await.ok();
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::OutFailed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_chat_media() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "bar").await?;
|
||||
let chat_id1 = create_group(&t, "foo").await?;
|
||||
let chat_id2 = create_group(&t, "bar").await?;
|
||||
|
||||
assert_eq!(
|
||||
get_chat_media(
|
||||
@@ -3380,7 +3405,7 @@ async fn test_get_chat_media_webxdc_order() -> Result<()> {
|
||||
async fn test_blob_renaming() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
let chat_id = create_group(&alice, "Group").await?;
|
||||
add_contact_to_chat(&alice, chat_id, alice.add_or_lookup_contact_id(&bob).await).await?;
|
||||
let file = alice.get_blobdir().join("harmless_file.\u{202e}txt.exe");
|
||||
fs::write(&file, "aaa").await?;
|
||||
@@ -3442,9 +3467,7 @@ async fn test_sync_blocked() -> Result<()> {
|
||||
// - Group chats synchronisation.
|
||||
// - That blocking a group deletes it on other devices.
|
||||
let fiona = TestContext::new_fiona().await;
|
||||
let fiona_grp_chat_id = fiona
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[alice0])
|
||||
.await;
|
||||
let fiona_grp_chat_id = fiona.create_group_with_members("grp", &[alice0]).await;
|
||||
let sent_msg = fiona.send_text(fiona_grp_chat_id, "hi").await;
|
||||
let a0_grp_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
|
||||
let a1_grp_chat_id = alice1.recv_msg(&sent_msg).await.chat_id;
|
||||
@@ -3577,9 +3600,7 @@ async fn test_sync_delete_chat() -> Result<()> {
|
||||
.get_matching(|evt| matches!(evt, EventType::ChatDeleted { .. }))
|
||||
.await;
|
||||
|
||||
let bob_grp_chat_id = bob
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[alice0])
|
||||
.await;
|
||||
let bob_grp_chat_id = bob.create_group_with_members("grp", &[alice0]).await;
|
||||
let sent_msg = bob.send_text(bob_grp_chat_id, "hi").await;
|
||||
let a0_grp_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
|
||||
let a1_grp_chat_id = alice1.recv_msg(&sent_msg).await.chat_id;
|
||||
@@ -3812,6 +3833,61 @@ async fn test_sync_name() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_create_group() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice0 = &tcm.alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let bob = &tcm.bob().await;
|
||||
let a0_bob_contact_id = alice0.add_or_lookup_contact_id(bob).await;
|
||||
let a1_bob_contact_id = alice1.add_or_lookup_contact_id(bob).await;
|
||||
let a0_chat_id = create_group(alice0, "grp").await?;
|
||||
sync(alice0, alice1).await;
|
||||
let a0_chat = Chat::load_from_db(alice0, a0_chat_id).await?;
|
||||
let a1_chat_id = get_chat_id_by_grpid(alice1, &a0_chat.grpid)
|
||||
.await?
|
||||
.unwrap()
|
||||
.0;
|
||||
let a1_chat = Chat::load_from_db(alice1, a1_chat_id).await?;
|
||||
assert_eq!(a1_chat.get_type(), Chattype::Group);
|
||||
assert_eq!(a1_chat.is_promoted(), false);
|
||||
assert_eq!(a1_chat.get_name(), "grp");
|
||||
|
||||
set_chat_name(alice0, a0_chat_id, "renamed").await?;
|
||||
sync(alice0, alice1).await;
|
||||
let a1_chat = Chat::load_from_db(alice1, a1_chat_id).await?;
|
||||
assert_eq!(a1_chat.is_promoted(), false);
|
||||
assert_eq!(a1_chat.get_name(), "renamed");
|
||||
|
||||
add_contact_to_chat(alice0, a0_chat_id, a0_bob_contact_id).await?;
|
||||
sync(alice0, alice1).await;
|
||||
let a1_chat = Chat::load_from_db(alice1, a1_chat_id).await?;
|
||||
assert_eq!(a1_chat.is_promoted(), false);
|
||||
assert_eq!(
|
||||
get_chat_contacts(alice1, a1_chat_id).await?,
|
||||
[a1_bob_contact_id, ContactId::SELF]
|
||||
);
|
||||
|
||||
// Let's test a contact removal from another device.
|
||||
remove_contact_from_chat(alice1, a1_chat_id, a1_bob_contact_id).await?;
|
||||
sync(alice1, alice0).await;
|
||||
let a0_chat = Chat::load_from_db(alice0, a0_chat_id).await?;
|
||||
assert_eq!(a0_chat.is_promoted(), false);
|
||||
assert_eq!(
|
||||
get_chat_contacts(alice0, a0_chat_id).await?,
|
||||
[ContactId::SELF]
|
||||
);
|
||||
|
||||
let sent_msg = alice0.send_text(a0_chat_id, "hi").await;
|
||||
let msg = alice1.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, a1_chat_id);
|
||||
assert_eq!(a1_chat_id.is_promoted(alice1).await?, true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests sending JPEG image with .png extension.
|
||||
///
|
||||
/// This is a regression test, previously sending failed
|
||||
@@ -3946,9 +4022,7 @@ async fn test_info_contact_id() -> Result<()> {
|
||||
}
|
||||
|
||||
// Alice creates group, Bob receives group
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "play", &[bob])
|
||||
.await;
|
||||
let alice_chat_id = alice.create_group_with_members("play", &[bob]).await;
|
||||
let sent_msg1 = alice.send_text(alice_chat_id, "moin").await;
|
||||
|
||||
let msg = bob.recv_msg(&sent_msg1).await;
|
||||
@@ -3994,26 +4068,27 @@ async fn test_info_contact_id() -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let fiona_id = alice.add_or_lookup_contact_id(&tcm.fiona().await).await; // contexts are in sync, fiona_id is same everywhere
|
||||
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
|
||||
let alice_fiona_id = alice.add_or_lookup_contact_id(&tcm.fiona().await).await;
|
||||
let bob_fiona_id = bob.add_or_lookup_contact_id(&tcm.fiona().await).await;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_fiona_id).await?;
|
||||
pop_recv_and_check(
|
||||
alice,
|
||||
alice2,
|
||||
bob,
|
||||
SystemMessage::MemberAddedToGroup,
|
||||
fiona_id,
|
||||
fiona_id,
|
||||
alice_fiona_id,
|
||||
bob_fiona_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
remove_contact_from_chat(alice, alice_chat_id, fiona_id).await?;
|
||||
remove_contact_from_chat(alice, alice_chat_id, alice_fiona_id).await?;
|
||||
pop_recv_and_check(
|
||||
alice,
|
||||
alice2,
|
||||
bob,
|
||||
SystemMessage::MemberRemovedFromGroup,
|
||||
fiona_id,
|
||||
fiona_id,
|
||||
alice_fiona_id,
|
||||
bob_fiona_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -4021,11 +4096,12 @@ async fn test_info_contact_id() -> Result<()> {
|
||||
// We raw delete in db as Contact::delete() leaves a tombstone (which is great as the tap works longer then)
|
||||
alice
|
||||
.sql
|
||||
.execute("DELETE FROM contacts WHERE id=?", (fiona_id,))
|
||||
.execute("DELETE FROM contacts WHERE id=?", (alice_fiona_id,))
|
||||
.await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::MemberRemovedFromGroup);
|
||||
assert!(msg.get_info_contact_id(alice).await?.is_none());
|
||||
assert!(msg.get_info_contact_id(bob).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4043,8 +4119,7 @@ async fn test_add_member_bug() -> Result<()> {
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
// Create a group.
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let alice_chat_id = create_group(alice, "Group chat").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
|
||||
@@ -4088,8 +4163,7 @@ async fn test_past_members() -> Result<()> {
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
tcm.section("Alice creates a chat.");
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let alice_chat_id = create_group(alice, "Group chat").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
alice
|
||||
.send_text(alice_chat_id, "Hi! I created a group.")
|
||||
@@ -4115,7 +4189,7 @@ async fn test_past_members() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn non_member_cannot_modify_member_list() -> Result<()> {
|
||||
async fn test_non_member_cannot_modify_member_list() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
@@ -4123,8 +4197,7 @@ async fn non_member_cannot_modify_member_list() -> Result<()> {
|
||||
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let alice_chat_id = create_group(alice, "Group chat").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let alice_sent_msg = alice
|
||||
.send_text(alice_chat_id, "Hi! I created a group.")
|
||||
@@ -4147,6 +4220,12 @@ async fn non_member_cannot_modify_member_list() -> Result<()> {
|
||||
alice.recv_msg_trash(&bob_sent_add_msg).await;
|
||||
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
// The same for removal.
|
||||
let bob_alice_contact_id = bob.add_or_lookup_contact_id(alice).await;
|
||||
remove_contact_from_chat(bob, bob_chat_id, bob_alice_contact_id).await?;
|
||||
let bob_sent_add_msg = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&bob_sent_add_msg).await;
|
||||
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4161,8 +4240,7 @@ async fn unpromoted_group_no_tombstones() -> Result<()> {
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let alice_chat_id = create_group(alice, "Group chat").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 3);
|
||||
@@ -4193,8 +4271,7 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
|
||||
let fiona = &tcm.fiona().await;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let alice_chat_id = create_group(alice, "Group chat").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
alice
|
||||
.send_text(alice_chat_id, "Hi! I created a group.")
|
||||
@@ -4231,7 +4308,7 @@ async fn test_past_members_order() -> Result<()> {
|
||||
let fiona = tcm.fiona().await;
|
||||
let fiona_contact_id = t.add_or_lookup_contact_id(&fiona).await;
|
||||
|
||||
let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let chat_id = create_group(t, "Group chat").await?;
|
||||
add_contact_to_chat(t, chat_id, bob_contact_id).await?;
|
||||
add_contact_to_chat(t, chat_id, charlie_contact_id).await?;
|
||||
add_contact_to_chat(t, chat_id, fiona_contact_id).await?;
|
||||
@@ -4293,8 +4370,7 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_charlie_contact_id = alice.add_or_lookup_contact_id(charlie).await;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let alice_chat_id = create_group(alice, "Group chat").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_charlie_contact_id).await?;
|
||||
|
||||
@@ -4482,9 +4558,7 @@ async fn test_cannot_send_edit_request() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[bob])
|
||||
.await;
|
||||
let chat_id = alice.create_group_with_members("My Group", &[bob]).await;
|
||||
|
||||
// Alice can edit her message
|
||||
let sent1 = alice.send_text(chat_id, "foo").await;
|
||||
@@ -4520,17 +4594,6 @@ async fn test_cannot_send_edit_request() -> Result<()> {
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// Videochat invitations cannot be edited
|
||||
alice
|
||||
.set_config(Config::WebrtcInstance, Some("https://foo.bar"))
|
||||
.await?;
|
||||
let msg_id = send_videochat_invitation(alice, chat_id).await?;
|
||||
assert!(
|
||||
send_edit_request(alice, msg_id, "bar".to_string())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// If not text was given initally, there is nothing to edit
|
||||
// (this also avoids complexity in UI element changes; focus is typos and rewordings)
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
@@ -4666,7 +4729,7 @@ async fn test_no_address_contacts_in_group_chats() -> Result<()> {
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let chat_id = create_group(alice, "Group chat").await?;
|
||||
let bob_key_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let charlie_address_contact_id = alice.add_or_lookup_address_contact_id(charlie).await;
|
||||
|
||||
@@ -4725,7 +4788,7 @@ async fn test_create_unencrypted_group_chat() -> Result<()> {
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let chat_id = create_group_ex(alice, None, "Group chat").await?;
|
||||
let chat_id = create_group_unencrypted(alice, "Group chat").await?;
|
||||
let bob_key_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let charlie_address_contact_id = alice.add_or_lookup_address_contact_id(charlie).await;
|
||||
|
||||
@@ -4742,6 +4805,16 @@ async fn test_create_unencrypted_group_chat() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_group_invalid_name() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let chat_id = create_group(alice, " ").await?;
|
||||
let chat = Chat::load_from_db(alice, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "…");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that avatar cannot be set in ad hoc groups.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_avatar_in_adhoc_chats() -> Result<()> {
|
||||
@@ -4772,3 +4845,25 @@ async fn test_no_avatar_in_adhoc_chats() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that long group name with non-ASCII characters is correctly received
|
||||
/// by other members.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_long_group_name() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let group_name = "δδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδ";
|
||||
let alice_chat_id = create_group(alice, group_name).await?;
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent = alice
|
||||
.send_text(alice_chat_id, "Hi! I created a group.")
|
||||
.await;
|
||||
let bob_chat_id = bob.recv_msg(&sent).await.chat_id;
|
||||
let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?;
|
||||
assert_eq!(bob_chat.name, group_name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -245,9 +245,6 @@ 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
|
||||
@@ -484,27 +481,23 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::chat::save_msgs;
|
||||
use crate::chat::{
|
||||
ProtectionStatus, add_contact_to_chat, create_group_chat, get_chat_contacts,
|
||||
remove_contact_from_chat, send_text_msg,
|
||||
add_contact_to_chat, create_group, get_chat_contacts, remove_contact_from_chat,
|
||||
send_text_msg,
|
||||
};
|
||||
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_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();
|
||||
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();
|
||||
|
||||
// check that the chatlist starts with the most recent message
|
||||
let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap();
|
||||
@@ -513,6 +506,8 @@ 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
|
||||
@@ -535,9 +530,7 @@ mod tests {
|
||||
|
||||
// receive a message from alice
|
||||
let alice = &tcm.alice().await;
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "alice chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let alice_chat_id = create_group(alice, "alice chat").await.unwrap();
|
||||
add_contact_to_chat(
|
||||
alice,
|
||||
alice_chat_id,
|
||||
@@ -575,9 +568,7 @@ mod tests {
|
||||
async fn test_sort_self_talk_up_on_forward() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.update_device_chats().await.unwrap();
|
||||
create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
create_group(&t, "a chat").await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 3);
|
||||
@@ -764,9 +755,7 @@ 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_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id1 = create_group(&t, "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();
|
||||
@@ -782,9 +771,7 @@ mod tests {
|
||||
async fn test_get_summary_deleted_draft() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id = create_group(&t, "a chat").await.unwrap();
|
||||
let mut msg = Message::new_text("Foobar".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
|
||||
@@ -823,15 +810,9 @@ 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_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();
|
||||
let chat_id1 = create_group(&t, "a chat").await.unwrap();
|
||||
create_group(&t, "b chat").await.unwrap();
|
||||
create_group(&t, "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,38 +1,39 @@
|
||||
//! Implementation of Consistent Color Generation.
|
||||
//! Color generation.
|
||||
//!
|
||||
//! 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;
|
||||
//! 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};
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
/// Converts an identifier to Hue angle.
|
||||
fn str_to_angle(s: &str) -> f64 {
|
||||
fn str_to_angle(s: &str) -> f32 {
|
||||
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));
|
||||
f64::from(checksum) / 65536.0 * 360.0
|
||||
f32::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((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
|
||||
fn rgb_to_u32(rgb: Rgb<u8>) -> u32 {
|
||||
65536 * u32::from(rgb.r) + 256 * u32::from(rgb.g) + u32::from(rgb.b)
|
||||
}
|
||||
|
||||
/// Converts an identifier to RGB color.
|
||||
///
|
||||
/// 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.
|
||||
/// Lightness is set to half (0.5) to make colors suitable both for light and dark theme.
|
||||
pub fn str_to_color(s: &str) -> u32 {
|
||||
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
|
||||
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)
|
||||
}
|
||||
|
||||
/// Returns color as a "#RRGGBB" `String` where R, G, B are hex digits.
|
||||
@@ -45,6 +46,7 @@ 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>
|
||||
@@ -57,11 +59,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_rgb_to_u32() {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ 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};
|
||||
@@ -23,6 +22,7 @@ use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::{Provider, get_provider_by_id};
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::get_abs_path;
|
||||
use crate::{constants, stats};
|
||||
|
||||
/// The available configuration keys.
|
||||
#[derive(
|
||||
@@ -151,10 +151,6 @@ 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"))]
|
||||
@@ -350,9 +346,6 @@ 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,
|
||||
|
||||
@@ -396,12 +389,6 @@ 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"))]
|
||||
@@ -417,23 +404,26 @@ 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,
|
||||
|
||||
/// This key is sent to the self_reporting bot so that the bot can recognize the user
|
||||
/// 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
|
||||
/// without storing the email address
|
||||
SelfReportingId,
|
||||
StatsId,
|
||||
|
||||
/// The last contact id that already existed when statistics-sending was enabled for the first time.
|
||||
StatsLastOldContactId,
|
||||
|
||||
/// MsgId of webxdc map integration.
|
||||
WebxdcIntegration,
|
||||
@@ -454,6 +444,9 @@ 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 {
|
||||
@@ -596,8 +589,9 @@ 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_parsed::<i32>(key)
|
||||
.get_config(key)
|
||||
.await?
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
.map(|x| x != 0)
|
||||
.unwrap_or_default())
|
||||
}
|
||||
@@ -705,7 +699,6 @@ impl Context {
|
||||
Config::Socks5Enabled
|
||||
| Config::ProxyEnabled
|
||||
| Config::BccSelf
|
||||
| Config::E2eeEnabled
|
||||
| Config::MdnsEnabled
|
||||
| Config::SentboxWatch
|
||||
| Config::MvboxMove
|
||||
@@ -734,12 +727,18 @@ impl Context {
|
||||
Self::check_config(key, value)?;
|
||||
|
||||
let _pause = match key.needs_io_restart() {
|
||||
true => self.scheduler.pause(self.clone()).await?,
|
||||
true => self.scheduler.pause(self).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::SentboxWatch {
|
||||
self.last_full_folder_scan.lock().await.take();
|
||||
if key == Config::StatsSending {
|
||||
stats::maybe_send_stats(self).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -892,6 +891,10 @@ 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.
|
||||
|
||||
@@ -36,6 +36,7 @@ use crate::login_param::{
|
||||
use crate::message::Message;
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
|
||||
use crate::qr::{login_param_from_account_qr, login_param_from_login_qr};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::time;
|
||||
@@ -117,7 +118,7 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
|
||||
pub(crate) async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
|
||||
ensure!(
|
||||
!self.scheduler.is_running().await,
|
||||
"cannot configure, already running"
|
||||
@@ -137,7 +138,7 @@ impl Context {
|
||||
|
||||
let res = self
|
||||
.inner_configure(param)
|
||||
.race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled"))))
|
||||
.race(cancel_channel.recv().map(|_| Err(format_err!("Canceled"))))
|
||||
.await;
|
||||
|
||||
self.free_ongoing().await;
|
||||
@@ -162,20 +163,15 @@ impl Context {
|
||||
pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
|
||||
self.stop_io().await;
|
||||
|
||||
// This code first sets the deprecated Config::Addr, Config::MailPw, etc.
|
||||
// and then calls configure(), which loads them again.
|
||||
// At some point, we will remove configure()
|
||||
// and then simplify the code
|
||||
// to directly create an EnteredLoginParam.
|
||||
let result = async move {
|
||||
match crate::qr::check_qr(self, qr).await? {
|
||||
crate::qr::Qr::Account { .. } => crate::qr::set_account_from_qr(self, qr).await?,
|
||||
let mut param = match crate::qr::check_qr(self, qr).await? {
|
||||
crate::qr::Qr::Account { .. } => login_param_from_account_qr(self, qr).await?,
|
||||
crate::qr::Qr::Login { address, options } => {
|
||||
crate::qr::configure_from_login_qr(self, &address, options).await?
|
||||
login_param_from_login_qr(&address, options)?
|
||||
}
|
||||
_ => bail!("QR code does not contain account"),
|
||||
}
|
||||
self.configure().await?;
|
||||
};
|
||||
self.add_transport_inner(&mut param).await?;
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
@@ -300,8 +296,6 @@ async fn get_configured_param(
|
||||
param.smtp.password.clone()
|
||||
};
|
||||
|
||||
let proxy_enabled = ctx.get_config_bool(Config::ProxyEnabled).await?;
|
||||
|
||||
let mut addr = param.addr.clone();
|
||||
if param.oauth2 {
|
||||
// the used oauth2 addr may differ, check this.
|
||||
@@ -343,7 +337,7 @@ async fn get_configured_param(
|
||||
"checking internal provider-info for offline autoconfig"
|
||||
);
|
||||
|
||||
provider = provider::get_provider_info(ctx, ¶m_domain, proxy_enabled).await;
|
||||
provider = provider::get_provider_info(¶m_domain);
|
||||
if let Some(provider) = provider {
|
||||
if provider.server.is_empty() {
|
||||
info!(ctx, "Offline autoconfig found, but no servers defined.");
|
||||
|
||||
@@ -106,7 +106,7 @@ fn parse_server<B: BufRead>(
|
||||
}
|
||||
}
|
||||
Event::Text(ref event) => {
|
||||
let val = event.unescape().unwrap_or_default().trim().to_owned();
|
||||
let val = event.xml_content().unwrap_or_default().trim().to_owned();
|
||||
|
||||
match tag_config {
|
||||
MozConfigTag::Hostname => hostname = Some(val),
|
||||
|
||||
@@ -79,7 +79,7 @@ fn parse_protocol<B: BufRead>(
|
||||
}
|
||||
}
|
||||
Event::Text(ref e) => {
|
||||
let val = e.unescape().unwrap_or_default();
|
||||
let val = e.xml_content().unwrap_or_default();
|
||||
|
||||
if let Some(ref tag) = current_tag {
|
||||
match tag.as_str() {
|
||||
@@ -123,7 +123,7 @@ fn parse_redirecturl<B: BufRead>(
|
||||
let mut buf = Vec::new();
|
||||
match reader.read_event_into(&mut buf)? {
|
||||
Event::Text(ref e) => {
|
||||
let val = e.unescape().unwrap_or_default();
|
||||
let val = e.xml_content().unwrap_or_default();
|
||||
Ok(val.trim().to_string())
|
||||
}
|
||||
_ => Ok("".to_string()),
|
||||
|
||||
@@ -60,23 +60,6 @@ pub enum MediaQuality {
|
||||
Worse = 1,
|
||||
}
|
||||
|
||||
/// Video chat URL type.
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(i8)]
|
||||
pub enum VideochatType {
|
||||
/// Unknown type.
|
||||
#[default]
|
||||
Unknown = 0,
|
||||
|
||||
/// [basicWebRTC](https://github.com/cracker0dks/basicwebrtc) instance.
|
||||
BasicWebrtc = 1,
|
||||
|
||||
/// [Jitsi Meet](https://jitsi.org/jitsi-meet/) instance.
|
||||
Jitsi = 2,
|
||||
}
|
||||
|
||||
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
|
||||
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
|
||||
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
|
||||
@@ -95,10 +78,11 @@ pub const DC_GCL_ADDRESS: u32 = 0x04;
|
||||
pub(crate) const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
|
||||
|
||||
// warn about an outdated app after a given number of days.
|
||||
// as we use the "provider-db generation date" as reference (that might not be updated very often)
|
||||
// and as not all system get speedy updates,
|
||||
// reference is the release date.
|
||||
// as not all system get speedy updates,
|
||||
// do not use too small value that will annoy users checking for nonexistent updates.
|
||||
pub(crate) const DC_OUTDATED_WARNING_DAYS: i64 = 365;
|
||||
// "90 days" has proven to be too short at some point (user were informed but there was no update)
|
||||
pub(crate) const DC_OUTDATED_WARNING_DAYS: i64 = 183;
|
||||
|
||||
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
|
||||
pub const DC_CHAT_ID_TRASH: ChatId = ChatId::new(3);
|
||||
@@ -117,6 +101,8 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
@@ -134,7 +120,7 @@ pub enum Chattype {
|
||||
|
||||
/// Group chat.
|
||||
///
|
||||
/// Created by [`crate::chat::create_group_chat`].
|
||||
/// Created by [`crate::chat::create_group`].
|
||||
Group = 120,
|
||||
|
||||
/// An (unencrypted) mailing list,
|
||||
@@ -307,16 +293,4 @@ mod tests {
|
||||
assert_eq!(MediaQuality::Balanced, MediaQuality::from_i32(0).unwrap());
|
||||
assert_eq!(MediaQuality::Worse, MediaQuality::from_i32(1).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_videochattype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(VideochatType::Unknown, VideochatType::default());
|
||||
assert_eq!(VideochatType::Unknown, VideochatType::from_i32(0).unwrap());
|
||||
assert_eq!(
|
||||
VideochatType::BasicWebrtc,
|
||||
VideochatType::from_i32(1).unwrap()
|
||||
);
|
||||
assert_eq!(VideochatType::Jitsi, VideochatType::from_i32(2).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
135
src/contact.rs
135
src/contact.rs
@@ -21,7 +21,7 @@ use tokio::task;
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::chat::ChatId;
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{self, Blocked, Chattype};
|
||||
@@ -36,7 +36,7 @@ use crate::message::MessageState;
|
||||
use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::sync::{self, Sync::*};
|
||||
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time};
|
||||
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time, to_lowercase};
|
||||
use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str};
|
||||
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
@@ -755,7 +755,19 @@ impl Contact {
|
||||
self.is_bot
|
||||
}
|
||||
|
||||
/// Check if an e-mail address belongs to a known and unblocked 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.
|
||||
///
|
||||
/// Known and unblocked contacts will be returned by `get_contacts()`.
|
||||
///
|
||||
@@ -795,14 +807,28 @@ impl Contact {
|
||||
.query_get_value(
|
||||
"SELECT id FROM contacts
|
||||
WHERE addr=?1 COLLATE NOCASE
|
||||
AND fingerprint='' -- Do not lookup key-contacts
|
||||
AND id>?2 AND origin>=?3 AND (? OR blocked=?)",
|
||||
AND id>?2 AND origin>=?3 AND (? OR blocked=?)
|
||||
ORDER BY
|
||||
(
|
||||
SELECT COUNT(*) FROM chats c
|
||||
INNER JOIN chats_contacts cc
|
||||
ON c.id=cc.chat_id
|
||||
WHERE c.type=?
|
||||
AND c.id>?
|
||||
AND c.blocked=?
|
||||
AND cc.contact_id=contacts.id
|
||||
) DESC,
|
||||
last_seen DESC, fingerprint DESC
|
||||
LIMIT 1",
|
||||
(
|
||||
&addr_normalized,
|
||||
ContactId::LAST_SPECIAL,
|
||||
min_origin as u32,
|
||||
blocked.is_none(),
|
||||
blocked.unwrap_or_default(),
|
||||
blocked.unwrap_or(Blocked::Not),
|
||||
Chattype::Single,
|
||||
constants::DC_CHAT_ID_LAST_SPECIAL,
|
||||
blocked.unwrap_or(Blocked::Not),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
@@ -1256,14 +1282,10 @@ impl Contact {
|
||||
|
||||
let list = context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY last_seen DESC, id DESC;",
|
||||
(ContactId::LAST_SPECIAL,),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
ids.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(list)
|
||||
@@ -1538,7 +1560,7 @@ impl Contact {
|
||||
return Ok(Some(chat::get_device_icon(context).await?));
|
||||
}
|
||||
if show_fallback_icon && !self.id.is_special() && !self.is_key_contact() {
|
||||
return Ok(Some(chat::get_address_contact_icon(context).await?));
|
||||
return Ok(Some(chat::get_unencrypted_icon(context).await?));
|
||||
}
|
||||
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if !image_rel.is_empty() {
|
||||
@@ -1548,12 +1570,10 @@ impl Contact {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Get a color for the contact.
|
||||
/// The color is calculated from the contact's email address
|
||||
/// and can be used for an fallback avatar with white initials
|
||||
/// as well as for headlines in bubbles of group chats.
|
||||
/// Returns a color for the contact.
|
||||
/// See [`self::get_color`].
|
||||
pub fn get_color(&self) -> u32 {
|
||||
str_to_color(&self.addr.to_lowercase())
|
||||
get_color(self.id == ContactId::SELF, &self.addr, &self.fingerprint())
|
||||
}
|
||||
|
||||
/// Gets the contact's status.
|
||||
@@ -1619,29 +1639,6 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if the contact profile title should display a green checkmark.
|
||||
///
|
||||
/// This generally should be consistent with the 1:1 chat with the contact
|
||||
/// so 1:1 chat with the contact and the contact profile
|
||||
/// either both display the green checkmark or both don't display a green checkmark.
|
||||
///
|
||||
/// UI often knows beforehand if a chat exists and can also call
|
||||
/// `chat.is_protected()` (if there is a chat)
|
||||
/// or `contact.is_verified()` (if there is no chat) directly.
|
||||
/// This is often easier and also skips some database calls.
|
||||
pub async fn is_profile_verified(&self, context: &Context) -> Result<bool> {
|
||||
let contact_id = self.id;
|
||||
|
||||
if let Some(ChatIdBlocked { id: chat_id, .. }) =
|
||||
ChatIdBlocked::lookup_by_contact(context, contact_id).await?
|
||||
{
|
||||
Ok(chat_id.is_protected(context).await? == ProtectionStatus::Protected)
|
||||
} else {
|
||||
// 1:1 chat does not exist.
|
||||
Ok(self.is_verified(context).await?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of real (i.e. non-special) contacts in the database.
|
||||
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
|
||||
if !context.sql.is_open().await {
|
||||
@@ -1672,6 +1669,21 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a color for a contact having given attributes.
|
||||
///
|
||||
/// The color is calculated from contact's fingerprint (for key-contacts) or email address (for
|
||||
/// address-contacts; should be lowercased to avoid allocation) and can be used for an fallback
|
||||
/// avatar with white initials as well as for headlines in bubbles of group chats.
|
||||
pub fn get_color(is_self: bool, addr: &str, fingerprint: &Option<Fingerprint>) -> u32 {
|
||||
if let Some(fingerprint) = fingerprint {
|
||||
str_to_color(&fingerprint.hex())
|
||||
} else if is_self {
|
||||
0x808080
|
||||
} else {
|
||||
str_to_color(&to_lowercase(addr))
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the names of the chats which use the contact name.
|
||||
//
|
||||
// This is one of the few duplicated data, however, getting the chat list is easier this way.
|
||||
@@ -1734,8 +1746,7 @@ pub(crate) async fn set_blocked(
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!contact_id.is_special(),
|
||||
"Can't block special contact {}",
|
||||
contact_id
|
||||
"Can't block special contact {contact_id}"
|
||||
);
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
|
||||
@@ -1775,9 +1786,7 @@ WHERE type=? AND id IN (
|
||||
// also unblock mailinglist
|
||||
// if the contact is a mailinglist address explicitly created to allow unblocking
|
||||
if !new_blocking && contact.origin == Origin::MailinglistAddress {
|
||||
if let Some((chat_id, _, _)) =
|
||||
chat::get_chat_id_by_grpid(context, &contact.addr).await?
|
||||
{
|
||||
if let Some((chat_id, ..)) = chat::get_chat_id_by_grpid(context, &contact.addr).await? {
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
}
|
||||
}
|
||||
@@ -1917,16 +1926,21 @@ pub(crate) async fn update_last_seen(
|
||||
}
|
||||
|
||||
/// Marks contact `contact_id` as verified by `verifier_id`.
|
||||
///
|
||||
/// `verifier_id == None` means that the verifier is unknown.
|
||||
pub(crate) async fn mark_contact_id_as_verified(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
verifier_id: ContactId,
|
||||
verifier_id: Option<ContactId>,
|
||||
) -> Result<()> {
|
||||
ensure_and_debug_assert_ne!(contact_id, ContactId::SELF,);
|
||||
ensure_and_debug_assert_ne!(
|
||||
contact_id,
|
||||
Some(contact_id),
|
||||
verifier_id,
|
||||
"Contact cannot be verified by self",
|
||||
);
|
||||
let by_self = verifier_id == Some(ContactId::SELF);
|
||||
let mut verifier_id = verifier_id.unwrap_or(contact_id);
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
@@ -1939,20 +1953,33 @@ pub(crate) async fn mark_contact_id_as_verified(
|
||||
bail!("Non-key-contact {contact_id} cannot be verified");
|
||||
}
|
||||
if verifier_id != ContactId::SELF {
|
||||
let verifier_fingerprint: String = transaction.query_row(
|
||||
"SELECT fingerprint FROM contacts WHERE id=?",
|
||||
(verifier_id,),
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
let (verifier_fingerprint, verifier_verifier_id): (String, ContactId) = transaction
|
||||
.query_row(
|
||||
"SELECT fingerprint, verifier FROM contacts WHERE id=?",
|
||||
(verifier_id,),
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)?;
|
||||
if verifier_fingerprint.is_empty() {
|
||||
bail!(
|
||||
"Contact {contact_id} cannot be verified by non-key-contact {verifier_id}"
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
verifier_id == contact_id || verifier_verifier_id != ContactId::UNDEFINED,
|
||||
"Contact {contact_id} cannot be verified by unverified contact {verifier_id}",
|
||||
);
|
||||
if verifier_verifier_id == verifier_id {
|
||||
// Avoid introducing incorrect reverse chains: if the verifier itself has an
|
||||
// unknown verifier, it may be `contact_id` actually (directly or indirectly) on
|
||||
// the other device (which is needed for getting "verified by unknown contact"
|
||||
// in the first place).
|
||||
verifier_id = contact_id;
|
||||
}
|
||||
}
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET verifier=? WHERE id=?",
|
||||
(verifier_id, contact_id),
|
||||
"UPDATE contacts SET verifier=?1
|
||||
WHERE id=?2 AND (verifier=0 OR verifier=id OR ?3)",
|
||||
(verifier_id, contact_id, by_self),
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::*;
|
||||
use crate::chat::{Chat, get_chat_contacts, send_text_msg};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
|
||||
|
||||
#[test]
|
||||
@@ -759,7 +760,7 @@ async fn test_contact_get_color() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = Contact::create(&t, "name", "name@example.net").await?;
|
||||
let color1 = Contact::get_by_id(&t, contact_id).await?.get_color();
|
||||
assert_eq!(color1, 0xA739FF);
|
||||
assert_eq!(color1, 0x4844e2);
|
||||
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = Contact::create(&t, "prename name", "name@example.net").await?;
|
||||
@@ -773,6 +774,20 @@ async fn test_contact_get_color() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_self_color_vs_key() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
t.configure_addr("alice@example.org").await;
|
||||
assert!(t.is_configured().await?);
|
||||
let color = Contact::get_by_id(t, ContactId::SELF).await?.get_color();
|
||||
assert_eq!(color, 0x808080);
|
||||
get_securejoin_qr(t, None).await?;
|
||||
let color1 = Contact::get_by_id(t, ContactId::SELF).await?.get_color();
|
||||
assert_ne!(color1, color);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_contact_get_encrinfo() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -1035,6 +1050,50 @@ async fn test_was_seen_recently_event() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_lookup_id_by_addr_recent_ex(accept_unencrypted_chat: bool) -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
|
||||
assert!(std::str::from_utf8(raw)?.contains("Date: Thu, 24 Nov 2022 20:05:57 +0100"));
|
||||
let received_msg = receive_imf(bob, raw, false).await?.unwrap();
|
||||
received_msg.chat_id.accept(bob).await?;
|
||||
|
||||
let raw = r#"From: Alice <alice@example.org>
|
||||
To: bob@example.net
|
||||
Message-ID: message$TIME@example.org
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
Date: Thu, 24 Nov 2022 $TIME +0100
|
||||
|
||||
Hi"#
|
||||
.to_string();
|
||||
for (time, is_key_contact) in [("20:05:57", true), ("20:05:58", !accept_unencrypted_chat)] {
|
||||
let raw = raw.replace("$TIME", time);
|
||||
let received_msg = receive_imf(bob, raw.as_bytes(), false).await?.unwrap();
|
||||
if accept_unencrypted_chat {
|
||||
received_msg.chat_id.accept(bob).await?;
|
||||
}
|
||||
let contact_id = Contact::lookup_id_by_addr(bob, "alice@example.org", Origin::Unknown)
|
||||
.await?
|
||||
.unwrap();
|
||||
let contact = Contact::get_by_id(bob, contact_id).await?;
|
||||
assert_eq!(contact.is_key_contact(), is_key_contact);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_lookup_id_by_addr_recent() -> Result<()> {
|
||||
let accept_unencrypted_chat = true;
|
||||
test_lookup_id_by_addr_recent_ex(accept_unencrypted_chat).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_lookup_id_by_addr_recent_accepted() -> Result<()> {
|
||||
let accept_unencrypted_chat = false;
|
||||
test_lookup_id_by_addr_recent_ex(accept_unencrypted_chat).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verified_by_none() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -1072,6 +1131,7 @@ async fn test_sync_create() -> Result<()> {
|
||||
.unwrap();
|
||||
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
|
||||
assert_eq!(a1b_contact.name, "Bob");
|
||||
assert_eq!(a1b_contact.is_key_contact(), false);
|
||||
|
||||
Contact::create(alice0, "Bob Renamed", "bob@example.net").await?;
|
||||
test_utils::sync(alice0, alice1).await;
|
||||
@@ -1081,6 +1141,7 @@ async fn test_sync_create() -> Result<()> {
|
||||
assert_eq!(id, a1b_contact_id);
|
||||
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
|
||||
assert_eq!(a1b_contact.name, "Bob Renamed");
|
||||
assert_eq!(a1b_contact.is_key_contact(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1256,13 +1317,9 @@ async fn test_self_is_verified() -> Result<()> {
|
||||
|
||||
let contact = Contact::get_by_id(&alice, ContactId::SELF).await?;
|
||||
assert_eq!(contact.is_verified(&alice).await?, true);
|
||||
assert!(contact.is_profile_verified(&alice).await?);
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
assert!(contact.is_key_contact());
|
||||
|
||||
let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?;
|
||||
assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
274
src/context.rs
274
src/context.rs
@@ -10,35 +10,31 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
use pgp::types::PublicKeyTrait;
|
||||
use ratelimit::Ratelimit;
|
||||
use tokio::sync::{Mutex, Notify, RwLock};
|
||||
|
||||
use crate::chat::{ChatId, ProtectionStatus, get_chat_cnt};
|
||||
use crate::chatlist_events;
|
||||
use crate::chat::{ChatId, get_chat_cnt};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR,
|
||||
};
|
||||
use crate::contact::{Contact, ContactId, import_vcard, mark_contact_id_as_verified};
|
||||
use crate::constants::{self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSION_STR};
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::debug_logging::DebugLogging;
|
||||
use crate::download::DownloadState;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
|
||||
use crate::key::{load_self_secret_key, self_fingerprint};
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::log::{info, warn};
|
||||
use crate::logged_debug_assert;
|
||||
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
|
||||
use crate::message::{self, Message, MessageState, MsgId};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
use crate::net::tls::TlsSessionStore;
|
||||
use crate::peer_channels::Iroh;
|
||||
use crate::push::PushSubscriber;
|
||||
use crate::quota::QuotaInfo;
|
||||
use crate::scheduler::{SchedulerState, convert_folder_meaning};
|
||||
use crate::scheduler::{ConnectivityStore, SchedulerState, convert_folder_meaning};
|
||||
use crate::sql::Sql;
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::timesmearing::SmearedTimestamp;
|
||||
use crate::tools::{self, create_id, duration_to_str, time, time_elapsed};
|
||||
use crate::tools::{self, duration_to_str, time, time_elapsed};
|
||||
use crate::{chatlist_events, stats};
|
||||
|
||||
/// Builder for the [`Context`].
|
||||
///
|
||||
@@ -262,8 +258,6 @@ pub struct InnerContext {
|
||||
/// IMAP METADATA.
|
||||
pub(crate) metadata: RwLock<Option<ServerMetadata>>,
|
||||
|
||||
pub(crate) last_full_folder_scan: Mutex<Option<tools::Time>>,
|
||||
|
||||
/// ID for this `Context` in the current process.
|
||||
///
|
||||
/// This allows for multiple `Context`s open in a single process where each context can
|
||||
@@ -297,6 +291,9 @@ pub struct InnerContext {
|
||||
/// True if account has subscribed to push notifications via IMAP.
|
||||
pub(crate) push_subscribed: AtomicBool,
|
||||
|
||||
/// TLS session resumption cache.
|
||||
pub(crate) tls_session_store: TlsSessionStore,
|
||||
|
||||
/// Iroh for realtime peer channels.
|
||||
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
|
||||
|
||||
@@ -304,6 +301,10 @@ pub struct InnerContext {
|
||||
/// tokio::sync::OnceCell would be possible to use, but overkill for our usecase;
|
||||
/// the standard library's OnceLock is enough, and it's a lot smaller in memory.
|
||||
pub(crate) self_fingerprint: OnceLock<String>,
|
||||
|
||||
/// `Connectivity` values for mailboxes, unordered. Used to compute the aggregate connectivity,
|
||||
/// see [`Context::get_connectivity()`].
|
||||
pub(crate) connectivities: parking_lot::Mutex<Vec<ConnectivityStore>>,
|
||||
}
|
||||
|
||||
/// The state of ongoing process.
|
||||
@@ -333,6 +334,15 @@ impl Default for RunningState {
|
||||
/// about the context on top of the information here.
|
||||
pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
let mut res = BTreeMap::new();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
res.insert(
|
||||
"debug_assertions",
|
||||
"On - DO NOT RELEASE THIS BUILD".to_string(),
|
||||
);
|
||||
#[cfg(not(debug_assertions))]
|
||||
res.insert("debug_assertions", "Off".to_string());
|
||||
|
||||
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
||||
res.insert("sqlite_version", rusqlite::version().to_string());
|
||||
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
||||
@@ -456,14 +466,15 @@ impl Context {
|
||||
server_id: RwLock::new(None),
|
||||
metadata: RwLock::new(None),
|
||||
creation_time: tools::Time::now(),
|
||||
last_full_folder_scan: Mutex::new(None),
|
||||
last_error: parking_lot::RwLock::new("".to_string()),
|
||||
migration_error: parking_lot::RwLock::new(None),
|
||||
debug_logging: std::sync::RwLock::new(None),
|
||||
push_subscriber,
|
||||
push_subscribed: AtomicBool::new(false),
|
||||
tls_session_store: TlsSessionStore::new(),
|
||||
iroh: Arc::new(RwLock::new(None)),
|
||||
self_fingerprint: OnceLock::new(),
|
||||
connectivities: parking_lot::Mutex::new(Vec::new()),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
@@ -493,7 +504,7 @@ impl Context {
|
||||
// Now, some configs may have changed, so, we need to invalidate the cache.
|
||||
self.sql.config_cache.write().await.clear();
|
||||
|
||||
self.scheduler.start(self.clone()).await;
|
||||
self.scheduler.start(self).await;
|
||||
}
|
||||
|
||||
/// Stops the IO scheduler.
|
||||
@@ -570,7 +581,7 @@ impl Context {
|
||||
} else {
|
||||
// Pause the scheduler to ensure another connection does not start
|
||||
// while we are fetching on a dedicated connection.
|
||||
let _pause_guard = self.scheduler.pause(self.clone()).await?;
|
||||
let _pause_guard = self.scheduler.pause(self).await?;
|
||||
|
||||
// Start a new dedicated connection.
|
||||
let mut connection = Imap::new_configured(self, channel::bounded(1).1).await?;
|
||||
@@ -807,7 +818,6 @@ impl Context {
|
||||
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await;
|
||||
let request_msgs = message::get_request_msg_cnt(self).await;
|
||||
let contacts = Contact::get_real_cnt(self).await?;
|
||||
let is_configured = self.get_config_int(Config::Configured).await?;
|
||||
let proxy_enabled = self.get_config_int(Config::ProxyEnabled).await?;
|
||||
let dbversion = self
|
||||
.sql
|
||||
@@ -819,7 +829,6 @@ impl Context {
|
||||
.query_get_value("PRAGMA journal_mode;", ())
|
||||
.await?
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await?;
|
||||
let sync_msgs = self.get_config_int(Config::SyncMsgs).await?;
|
||||
@@ -887,7 +896,6 @@ impl Context {
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert("is_configured", is_configured.to_string());
|
||||
res.insert("proxy_enabled", proxy_enabled.to_string());
|
||||
res.insert("entered_account_settings", l.to_string());
|
||||
res.insert("used_account_settings", l2);
|
||||
@@ -953,19 +961,12 @@ impl Context {
|
||||
res.insert("configured_mvbox_folder", configured_mvbox_folder);
|
||||
res.insert("configured_trash_folder", configured_trash_folder);
|
||||
res.insert("mdns_enabled", mdns_enabled.to_string());
|
||||
res.insert("e2ee_enabled", e2ee_enabled.to_string());
|
||||
res.insert("bcc_self", bcc_self.to_string());
|
||||
res.insert("sync_msgs", sync_msgs.to_string());
|
||||
res.insert("disable_idle", disable_idle.to_string());
|
||||
res.insert("private_key_count", prv_key_cnt.to_string());
|
||||
res.insert("public_key_count", pub_key_cnt.to_string());
|
||||
res.insert("fingerprint", fingerprint_str);
|
||||
res.insert(
|
||||
"webrtc_instance",
|
||||
self.get_config(Config::WebrtcInstance)
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert(
|
||||
"media_quality",
|
||||
self.get_config_int(Config::MediaQuality).await?.to_string(),
|
||||
@@ -1024,12 +1025,6 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"protect_autocrypt",
|
||||
self.get_config_int(Config::ProtectAutocrypt)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"debug_logging",
|
||||
self.get_config_int(Config::DebugLogging).await?.to_string(),
|
||||
@@ -1042,12 +1037,6 @@ impl Context {
|
||||
"gossip_period",
|
||||
self.get_config_int(Config::GossipPeriod).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"verified_one_on_one_chats",
|
||||
self.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"webxdc_realtime_enabled",
|
||||
self.get_config_bool(Config::WebxdcRealtimeEnabled)
|
||||
@@ -1067,6 +1056,29 @@ impl Context {
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
res.insert(
|
||||
"stats_id",
|
||||
self.get_config(Config::StatsId)
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert(
|
||||
"stats_sending",
|
||||
stats::should_send_stats(self).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"stats_last_sent",
|
||||
self.get_config_i64(Config::StatsLastSent)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"fail_on_receiving_full_msg",
|
||||
self.sql
|
||||
.get_raw_config("fail_on_receiving_full_msg")
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
let elapsed = time_elapsed(&self.creation_time);
|
||||
res.insert("uptime", duration_to_str(elapsed));
|
||||
@@ -1074,152 +1086,6 @@ impl Context {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn get_self_report(&self) -> Result<String> {
|
||||
#[derive(Default)]
|
||||
struct ChatNumbers {
|
||||
protected: u32,
|
||||
protection_broken: u32,
|
||||
opportunistic_dc: u32,
|
||||
opportunistic_mua: u32,
|
||||
unencrypted_dc: u32,
|
||||
unencrypted_mua: u32,
|
||||
}
|
||||
|
||||
let mut res = String::new();
|
||||
res += &format!("core_version {}\n", get_version_str());
|
||||
|
||||
let num_msgs: u32 = self
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id!=?",
|
||||
(DC_CHAT_ID_TRASH,),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
res += &format!("num_msgs {num_msgs}\n");
|
||||
|
||||
let num_chats: u32 = self
|
||||
.sql
|
||||
.query_get_value("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked!=1", ())
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
res += &format!("num_chats {num_chats}\n");
|
||||
|
||||
let db_size = tokio::fs::metadata(&self.sql.dbfile).await?.len();
|
||||
res += &format!("db_size_bytes {db_size}\n");
|
||||
|
||||
let secret_key = &load_self_secret_key(self).await?.primary_key;
|
||||
let key_created = secret_key.public_key().created_at().timestamp();
|
||||
res += &format!("key_created {key_created}\n");
|
||||
|
||||
// how many of the chats active in the last months are:
|
||||
// - protected
|
||||
// - protection-broken
|
||||
// - opportunistic-encrypted and the contact uses Delta Chat
|
||||
// - opportunistic-encrypted and the contact uses a classical MUA
|
||||
// - unencrypted and the contact uses Delta Chat
|
||||
// - unencrypted and the contact uses a classical MUA
|
||||
let three_months_ago = time().saturating_sub(3600 * 24 * 30 * 3);
|
||||
let chats = self
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.protected, m.param, m.msgrmsg
|
||||
FROM chats c
|
||||
JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND hidden=0
|
||||
AND download_state=?
|
||||
AND to_id!=?
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9
|
||||
AND (c.blocked=0 OR c.blocked=2)
|
||||
AND IFNULL(m.timestamp,c.created_timestamp) > ?
|
||||
GROUP BY c.id",
|
||||
(DownloadState::Done, ContactId::INFO, three_months_ago),
|
||||
|row| {
|
||||
let protected: ProtectionStatus = row.get(0)?;
|
||||
let message_param: Params =
|
||||
row.get::<_, String>(1)?.parse().unwrap_or_default();
|
||||
let is_dc_message: bool = row.get(2)?;
|
||||
Ok((protected, message_param, is_dc_message))
|
||||
},
|
||||
|rows| {
|
||||
let mut chats = ChatNumbers::default();
|
||||
for row in rows {
|
||||
let (protected, message_param, is_dc_message) = row?;
|
||||
let encrypted = message_param
|
||||
.get_bool(Param::GuaranteeE2ee)
|
||||
.unwrap_or(false);
|
||||
|
||||
if protected == ProtectionStatus::Protected {
|
||||
chats.protected += 1;
|
||||
} else if protected == ProtectionStatus::ProtectionBroken {
|
||||
chats.protection_broken += 1;
|
||||
} else if encrypted {
|
||||
if is_dc_message {
|
||||
chats.opportunistic_dc += 1;
|
||||
} else {
|
||||
chats.opportunistic_mua += 1;
|
||||
}
|
||||
} else if is_dc_message {
|
||||
chats.unencrypted_dc += 1;
|
||||
} else {
|
||||
chats.unencrypted_mua += 1;
|
||||
}
|
||||
}
|
||||
Ok(chats)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
res += &format!("chats_protected {}\n", chats.protected);
|
||||
res += &format!("chats_protection_broken {}\n", chats.protection_broken);
|
||||
res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc);
|
||||
res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua);
|
||||
res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc);
|
||||
res += &format!("chats_unencrypted_mua {}\n", chats.unencrypted_mua);
|
||||
|
||||
let self_reporting_id = match self.get_config(Config::SelfReportingId).await? {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
let id = create_id();
|
||||
self.set_config(Config::SelfReportingId, Some(&id)).await?;
|
||||
id
|
||||
}
|
||||
};
|
||||
res += &format!("self_reporting_id {self_reporting_id}");
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Drafts a message with statistics about the usage of Delta Chat.
|
||||
/// The user can inspect the message if they want, and then hit "Send".
|
||||
///
|
||||
/// On the other end, a bot will receive the message and make it available
|
||||
/// to Delta Chat's developers.
|
||||
pub async fn draft_self_report(&self) -> Result<ChatId> {
|
||||
const SELF_REPORTING_BOT_VCARD: &str = include_str!("../assets/self-reporting-bot.vcf");
|
||||
let contact_id: ContactId = *import_vcard(self, SELF_REPORTING_BOT_VCARD)
|
||||
.await?
|
||||
.first()
|
||||
.context("Self reporting bot vCard does not contain a contact")?;
|
||||
mark_contact_id_as_verified(self, contact_id, ContactId::SELF).await?;
|
||||
|
||||
let chat_id = ChatId::create_for_contact(self, contact_id).await?;
|
||||
chat_id
|
||||
.set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id))
|
||||
.await?;
|
||||
|
||||
let mut msg = Message::new_text(self.get_self_report().await?);
|
||||
|
||||
chat_id.set_draft(self, Some(&mut msg)).await?;
|
||||
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
/// Get a list of fresh, unmuted messages in unblocked chats.
|
||||
///
|
||||
/// The list starts with the most recent message
|
||||
@@ -1229,7 +1095,7 @@ impl Context {
|
||||
pub async fn get_fresh_msgs(&self) -> Result<Vec<MsgId>> {
|
||||
let list = self
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
@@ -1247,13 +1113,6 @@ impl Context {
|
||||
),
|
||||
(MessageState::InFresh, time()),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|rows| {
|
||||
let mut list = Vec::new();
|
||||
for row in rows {
|
||||
list.push(row?);
|
||||
}
|
||||
Ok(list)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(list)
|
||||
@@ -1286,7 +1145,7 @@ impl Context {
|
||||
|
||||
let list = self
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
"SELECT m.id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
@@ -1306,13 +1165,6 @@ impl Context {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
|rows| {
|
||||
let mut list = Vec::new();
|
||||
for row in rows {
|
||||
list.push(row?);
|
||||
}
|
||||
Ok(list)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(list)
|
||||
@@ -1353,7 +1205,7 @@ impl Context {
|
||||
|
||||
let list = if let Some(chat_id) = chat_id {
|
||||
self.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
"SELECT m.id AS id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
@@ -1365,13 +1217,6 @@ impl Context {
|
||||
ORDER BY m.timestamp,m.id;",
|
||||
(chat_id, str_like_in_text),
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for id in rows {
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -1386,7 +1231,7 @@ impl Context {
|
||||
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
|
||||
// The limit is documented and UI may add a hint when getting 1000 results.
|
||||
self.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
"SELECT m.id AS id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
@@ -1401,13 +1246,6 @@ impl Context {
|
||||
ORDER BY m.id DESC LIMIT 1000",
|
||||
(str_like_in_text,),
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for id in rows {
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
@@ -6,9 +6,9 @@ use super::*;
|
||||
use crate::chat::{Chat, MuteDuration, get_chat_contacts, get_chat_msgs, send_msg, set_muted};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::message::Message;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, get_chat_msg};
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext};
|
||||
use crate::tools::{SystemTime, create_outgoing_rfc724_mid};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -276,7 +276,6 @@ async fn test_get_info_completeness() {
|
||||
"mail_port",
|
||||
"mail_security",
|
||||
"notify_about_wrong_pw",
|
||||
"self_reporting_id",
|
||||
"selfstatus",
|
||||
"send_server",
|
||||
"send_user",
|
||||
@@ -296,6 +295,8 @@ async fn test_get_info_completeness() {
|
||||
"webxdc_integration",
|
||||
"device_token",
|
||||
"encrypted_device_token",
|
||||
"stats_last_update",
|
||||
"stats_last_old_contact_id",
|
||||
];
|
||||
let t = TestContext::new().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
@@ -598,26 +599,6 @@ async fn test_get_next_msgs() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_draft_self_report() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let chat_id = alice.draft_self_report().await?;
|
||||
let msg = get_chat_msg(&alice, chat_id, 0, 1).await;
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
||||
|
||||
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
|
||||
let mut draft = chat_id.get_draft(&alice).await?.unwrap();
|
||||
assert!(draft.text.starts_with("core_version"));
|
||||
|
||||
// Test that sending into the protected chat works:
|
||||
let _sent = alice.send_msg(chat_id, &mut draft).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::sync::LazyLock;
|
||||
|
||||
use quick_xml::{
|
||||
Reader,
|
||||
errors::Error as QuickXmlError,
|
||||
events::{BytesEnd, BytesStart, BytesText},
|
||||
};
|
||||
|
||||
@@ -132,6 +133,7 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
reader.config_mut().check_end_names = false;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut char_buf = String::with_capacity(4);
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
@@ -140,16 +142,9 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
}
|
||||
Ok(quick_xml::events::Event::End(ref e)) => dehtml_endtag_cb(e, &mut dehtml),
|
||||
Ok(quick_xml::events::Event::Text(ref e)) => dehtml_text_cb(e, &mut dehtml),
|
||||
Ok(quick_xml::events::Event::CData(e)) => match e.escape() {
|
||||
Ok(e) => dehtml_text_cb(&e, &mut dehtml),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"CDATA escape error at position {}: {:?}",
|
||||
reader.buffer_position(),
|
||||
e,
|
||||
);
|
||||
}
|
||||
},
|
||||
Ok(quick_xml::events::Event::CData(e)) => {
|
||||
str_cb(&String::from_utf8_lossy(&e as &[_]), &mut dehtml)
|
||||
}
|
||||
Ok(quick_xml::events::Event::Empty(ref e)) => {
|
||||
// Handle empty tags as a start tag immediately followed by end tag.
|
||||
// For example, `<p/>` is treated as `<p></p>`.
|
||||
@@ -159,6 +154,33 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
&mut dehtml,
|
||||
);
|
||||
}
|
||||
Ok(quick_xml::events::Event::GeneralRef(ref e)) => {
|
||||
match e.resolve_char_ref() {
|
||||
Err(err) => eprintln!(
|
||||
"resolve_char_ref() error at position {}: {:?}",
|
||||
reader.buffer_position(),
|
||||
err,
|
||||
),
|
||||
Ok(Some(ch)) => {
|
||||
char_buf.clear();
|
||||
char_buf.push(ch);
|
||||
str_cb(&char_buf, &mut dehtml);
|
||||
}
|
||||
Ok(None) => {
|
||||
let event_str = String::from_utf8_lossy(e);
|
||||
if let Some(s) = quick_xml::escape::resolve_html5_entity(&event_str) {
|
||||
str_cb(s, &mut dehtml);
|
||||
} else {
|
||||
// Nonstandard entity. Add escaped.
|
||||
str_cb(&format!("&{event_str};"), &mut dehtml);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(QuickXmlError::IllFormed(_)) => {
|
||||
// This is probably not HTML at all and should be left as is.
|
||||
str_cb(&String::from_utf8_lossy(&buf), &mut dehtml);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Parse html error: Error at position {}: {:?}",
|
||||
@@ -176,36 +198,36 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
}
|
||||
|
||||
fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
|
||||
static LINE_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
|
||||
|
||||
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|
||||
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
|
||||
{
|
||||
let event = event as &[_];
|
||||
let event_str = std::str::from_utf8(event).unwrap_or_default();
|
||||
let mut last_added = escaper::decode_html_buf_sloppy(event).unwrap_or_default();
|
||||
if event_str.starts_with(&last_added) {
|
||||
last_added = event_str.to_string();
|
||||
str_cb(event_str, dehtml);
|
||||
}
|
||||
}
|
||||
|
||||
fn str_cb(event_str: &str, dehtml: &mut Dehtml) {
|
||||
static LINE_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
|
||||
|
||||
let add_text = dehtml.get_add_text();
|
||||
if add_text == AddText::YesRemoveLineEnds {
|
||||
// Replace all line ends with spaces.
|
||||
// E.g. `\r\n\r\n` is replaced with one space.
|
||||
let event_str = LINE_RE.replace_all(event_str, " ");
|
||||
|
||||
// Add a space if `event_str` starts with a space
|
||||
// and there is no whitespace at the end of the buffer yet.
|
||||
// Trim the rest of leading whitespace from `event_str`.
|
||||
let buf = dehtml.get_buf();
|
||||
if !buf.ends_with(' ') && !buf.ends_with('\n') && event_str.starts_with(' ') {
|
||||
*buf += " ";
|
||||
}
|
||||
|
||||
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
|
||||
// Replace all line ends with spaces.
|
||||
// E.g. `\r\n\r\n` is replaced with one space.
|
||||
let last_added = LINE_RE.replace_all(&last_added, " ");
|
||||
|
||||
// Add a space if `last_added` starts with a space
|
||||
// and there is no whitespace at the end of the buffer yet.
|
||||
// Trim the rest of leading whitespace from `last_added`.
|
||||
let buf = dehtml.get_buf();
|
||||
if !buf.ends_with(' ') && !buf.ends_with('\n') && last_added.starts_with(' ') {
|
||||
*buf += " ";
|
||||
}
|
||||
|
||||
*buf += last_added.trim_start();
|
||||
} else {
|
||||
*dehtml.get_buf() += LINE_RE.replace_all(&last_added, "\n").as_ref();
|
||||
}
|
||||
*buf += event_str.trim_start();
|
||||
} else if add_text == AddText::YesPreserveLineEnds {
|
||||
*dehtml.get_buf() += LINE_RE.replace_all(event_str, "\n").as_ref();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -157,30 +157,23 @@ pub(crate) async fn download_msg(
|
||||
let row = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT uid, folder, uidvalidity FROM imap WHERE rfc724_mid=? AND target!=''",
|
||||
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
|
||||
(&msg.rfc724_mid,),
|
||||
|row| {
|
||||
let server_uid: u32 = row.get(0)?;
|
||||
let server_folder: String = row.get(1)?;
|
||||
let uidvalidity: u32 = row.get(2)?;
|
||||
Ok((server_uid, server_folder, uidvalidity))
|
||||
Ok((server_uid, server_folder))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Some((server_uid, server_folder, uidvalidity)) = row else {
|
||||
let Some((server_uid, server_folder)) = row else {
|
||||
// No IMAP record found, we don't know the UID and folder.
|
||||
return Err(anyhow!("Call download_full() again to try over."));
|
||||
};
|
||||
|
||||
session
|
||||
.fetch_single_msg(
|
||||
context,
|
||||
&server_folder,
|
||||
uidvalidity,
|
||||
server_uid,
|
||||
msg.rfc724_mid.clone(),
|
||||
)
|
||||
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -194,7 +187,6 @@ impl Session {
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
uidvalidity: u32,
|
||||
uid: u32,
|
||||
rfc724_mid: String,
|
||||
) -> Result<()> {
|
||||
@@ -214,16 +206,8 @@ impl Session {
|
||||
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
|
||||
uid_message_ids.insert(uid, rfc724_mid);
|
||||
let (sender, receiver) = async_channel::unbounded();
|
||||
self.fetch_many_msgs(
|
||||
context,
|
||||
folder,
|
||||
uidvalidity,
|
||||
vec![uid],
|
||||
&uid_message_ids,
|
||||
false,
|
||||
sender,
|
||||
)
|
||||
.await?;
|
||||
self.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, false, sender)
|
||||
.await?;
|
||||
if receiver.recv().await.is_err() {
|
||||
bail!("Failed to fetch UID {uid}");
|
||||
}
|
||||
@@ -238,14 +222,20 @@ impl MimeMessage {
|
||||
/// the mime-structure itself is not available.
|
||||
///
|
||||
/// The placeholder part currently contains a text with size and availability of the message;
|
||||
/// `error` is set as the part error;
|
||||
/// in the future, we may do more advanced things as previews here.
|
||||
pub(crate) async fn create_stub_from_partial_download(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
org_bytes: u32,
|
||||
error: Option<String>,
|
||||
) -> Result<()> {
|
||||
let prefix = match error {
|
||||
None => "",
|
||||
Some(_) => "[❗] ",
|
||||
};
|
||||
let mut text = format!(
|
||||
"[{}]",
|
||||
"{prefix}[{}]",
|
||||
stock_str::partial_download_msg_body(context, org_bytes).await
|
||||
);
|
||||
if let Some(delete_server_after) = context.get_config_delete_server_after().await? {
|
||||
@@ -259,9 +249,10 @@ impl MimeMessage {
|
||||
|
||||
info!(context, "Partial download: {}", text);
|
||||
|
||||
self.parts.push(Part {
|
||||
self.do_add_single_part(Part {
|
||||
typ: Viewtype::Text,
|
||||
msg: text,
|
||||
error,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
@@ -276,8 +267,9 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_msgs, send_msg};
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::message::delete_msgs;
|
||||
use crate::receive_imf::receive_imf_from_inbox;
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext};
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
fn test_downloadstate_values() {
|
||||
@@ -536,4 +528,43 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that fully downloading the message
|
||||
/// works even if the Message-ID already exists
|
||||
/// in the database assigned to the trash chat.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_partial_download_trashed() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let imf_raw = b"From: Bob <bob@example.org>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: subject\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\
|
||||
Content-Type: text/plain";
|
||||
|
||||
// Download message from Bob partially.
|
||||
let partial_received_msg =
|
||||
receive_imf_from_inbox(alice, "first@example.org", imf_raw, false, Some(100000))
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(partial_received_msg.msg_ids.len(), 1);
|
||||
|
||||
// Delete the received message.
|
||||
// Not it is still in the database,
|
||||
// but in the trash chat.
|
||||
delete_msgs(alice, &[partial_received_msg.msg_ids[0]]).await?;
|
||||
|
||||
// Fully download message after deletion.
|
||||
let full_received_msg =
|
||||
receive_imf_from_inbox(alice, "first@example.org", imf_raw, false, None).await?;
|
||||
|
||||
// The message does not reappear.
|
||||
// However, `receive_imf` should not fail.
|
||||
assert!(full_received_msg.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
15
src/e2ee.rs
15
src/e2ee.rs
@@ -4,10 +4,8 @@ use std::io::Cursor;
|
||||
|
||||
use anyhow::Result;
|
||||
use mail_builder::mime::MimePart;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::key::{SignedPublicKey, load_self_public_key, load_self_secret_key};
|
||||
use crate::pgp;
|
||||
@@ -21,9 +19,7 @@ pub struct EncryptHelper {
|
||||
|
||||
impl EncryptHelper {
|
||||
pub async fn new(context: &Context) -> Result<EncryptHelper> {
|
||||
let prefer_encrypt =
|
||||
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
|
||||
.unwrap_or_default();
|
||||
let prefer_encrypt = EncryptPreference::Mutual;
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
let public_key = load_self_public_key(context).await?;
|
||||
|
||||
@@ -35,9 +31,12 @@ impl EncryptHelper {
|
||||
}
|
||||
|
||||
pub fn get_aheader(&self) -> Aheader {
|
||||
let pk = self.public_key.clone();
|
||||
let addr = self.addr.to_string();
|
||||
Aheader::new(addr, pk, self.prefer_encrypt)
|
||||
Aheader {
|
||||
addr: self.addr.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
prefer_encrypt: self.prefer_encrypt,
|
||||
verified: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to encrypt the passed in `mail`.
|
||||
|
||||
@@ -80,12 +80,12 @@ use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::download::MIN_DELETE_SERVER_AFTER;
|
||||
use crate::events::EventType;
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{SystemTime, duration_to_str, time};
|
||||
use crate::{location, stats};
|
||||
|
||||
/// Ephemeral timer value.
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
|
||||
@@ -277,6 +277,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
.await
|
||||
}
|
||||
604_800 => stock_str::msg_ephemeral_timer_week(context, from_id).await,
|
||||
31_536_000..=31_708_800 => stock_str::msg_ephemeral_timer_year(context, from_id).await,
|
||||
_ => {
|
||||
stock_str::msg_ephemeral_timer_weeks(
|
||||
context,
|
||||
@@ -375,13 +376,17 @@ pub(crate) async fn start_chat_ephemeral_timers(context: &Context, chat_id: Chat
|
||||
/// `delete_device_after` setting or `ephemeral_timestamp` column.
|
||||
///
|
||||
/// For each message a row ID, chat id, viewtype and location ID is returned.
|
||||
///
|
||||
/// Unknown viewtypes are returned as `Viewtype::Unknown`
|
||||
/// and not as errors bubbled up, easily resulting in infinite loop or leaving messages undeleted.
|
||||
/// (Happens when viewtypes are removed or added on another device which was backup/add-second-device source)
|
||||
async fn select_expired_messages(
|
||||
context: &Context,
|
||||
now: i64,
|
||||
) -> Result<Vec<(MsgId, ChatId, Viewtype, u32)>> {
|
||||
let mut rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
r#"
|
||||
SELECT id, chat_id, type, location_id
|
||||
FROM msgs
|
||||
@@ -394,11 +399,14 @@ WHERE
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let viewtype: Viewtype = row.get("type")?;
|
||||
let viewtype: Viewtype = row
|
||||
.get("type")
|
||||
.context("Using default viewtype for ephemeral handling.")
|
||||
.log_err(context)
|
||||
.unwrap_or_default();
|
||||
let location_id: u32 = row.get("location_id")?;
|
||||
Ok((id, chat_id, viewtype, location_id))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -416,7 +424,7 @@ WHERE
|
||||
|
||||
let rows_expired = context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
r#"
|
||||
SELECT id, chat_id, type, location_id
|
||||
FROM msgs
|
||||
@@ -436,11 +444,14 @@ WHERE
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let viewtype: Viewtype = row.get("type")?;
|
||||
let viewtype: Viewtype = row
|
||||
.get("type")
|
||||
.context("Using default viewtype for delete-old handling.")
|
||||
.log_err(context)
|
||||
.unwrap_or_default();
|
||||
let location_id: u32 = row.get("location_id")?;
|
||||
Ok((id, chat_id, viewtype, location_id))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -597,7 +608,7 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
|
||||
+ Duration::from_secs(1)
|
||||
} else {
|
||||
// no messages to be deleted for now, wait long for one to occur
|
||||
now + Duration::from_secs(86400)
|
||||
now + Duration::from_secs(86400) // 1 day
|
||||
};
|
||||
|
||||
if let Ok(duration) = until.duration_since(now) {
|
||||
@@ -624,6 +635,12 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that the statistics stay correct by updating them _before_ deleting messages:
|
||||
stats::maybe_update_message_stats(context)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
delete_expired_messages(context, time())
|
||||
.await
|
||||
.log_err(context)
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE;
|
||||
use crate::{
|
||||
chat::{self, Chat, ChatItem, ProtectionStatus, create_group_chat, send_text_msg},
|
||||
chat::{self, Chat, ChatItem, create_group, send_text_msg},
|
||||
tools::IsNoneOrEmpty,
|
||||
};
|
||||
|
||||
@@ -128,31 +128,33 @@ async fn test_stock_ephemeral_messages() {
|
||||
/// Test enabling and disabling ephemeral timer remotely.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_enable_disable() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
let chat_alice = alice.create_chat(bob).await.id;
|
||||
let chat_bob = bob.create_chat(alice).await.id;
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
let bob_received_message = bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
bob_received_message.text,
|
||||
"Message deletion timer is set to 1 minute by alice@example.org."
|
||||
);
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(bob).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
||||
.set_ephemeral_timer(alice, Timer::Disabled)
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
assert_eq!(chat_bob.get_ephemeral_timer(bob).await?, Timer::Disabled);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -162,7 +164,7 @@ async fn test_ephemeral_enable_disable() -> Result<()> {
|
||||
async fn test_ephemeral_unpromoted() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?;
|
||||
let chat_id = create_group(&alice, "Group name").await?;
|
||||
|
||||
// Group is unpromoted, the timer can be changed without sending a message.
|
||||
assert!(chat_id.is_unpromoted(&alice).await?);
|
||||
@@ -797,8 +799,7 @@ async fn test_ephemeral_timer_non_member() -> Result<()> {
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group name").await?;
|
||||
let alice_chat_id = create_group(alice, "Group name").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
send_text_msg(alice, alice_chat_id, "Hi!".to_string()).await?;
|
||||
|
||||
@@ -824,3 +825,68 @@ async fn test_ephemeral_timer_non_member() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that expiration of a disappearing message
|
||||
/// with unknown viewtype does not make `delete_expired_messages` fail.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_disappearing_unknown_viewtype() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let chat = alice.create_chat(bob).await;
|
||||
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
|
||||
let mut msg = Message::new_text("Expiring message".to_string());
|
||||
let _alice_sent_message = alice.send_msg(chat.id, &mut msg).await;
|
||||
|
||||
// Set message viewtype to unassigned
|
||||
// type 70 that was previously used for videochat invitations.
|
||||
alice
|
||||
.sql
|
||||
.execute("UPDATE msgs SET type=70 WHERE id=?", (msg.id,))
|
||||
.await?;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
// This should not fail.
|
||||
delete_expired_messages(alice, time()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that deletion of a message with unknown viewtype
|
||||
/// triggered by `delete_device_after`
|
||||
/// does not make `delete_expired_messages` fail.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_device_after_unknown_viewtype() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let chat = alice.create_chat(bob).await;
|
||||
alice
|
||||
.set_config(Config::DeleteDeviceAfter, Some("600"))
|
||||
.await?;
|
||||
|
||||
let mut msg = Message::new_text("Some message".to_string());
|
||||
let _alice_sent_message = alice.send_msg(chat.id, &mut msg).await;
|
||||
|
||||
// Set message viewtype to unassigned
|
||||
// type 70 that was previously used for videochat invitations.
|
||||
alice
|
||||
.sql
|
||||
.execute("UPDATE msgs SET type=70 WHERE id=?", (msg.id,))
|
||||
.await?;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(1000));
|
||||
|
||||
// This should not fail.
|
||||
delete_expired_messages(alice, time()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -66,8 +66,7 @@ mod test_chatlist_events {
|
||||
use crate::{
|
||||
EventType,
|
||||
chat::{
|
||||
self, ChatId, ChatVisibility, MuteDuration, ProtectionStatus, create_broadcast,
|
||||
create_group_chat, set_muted,
|
||||
self, ChatId, ChatVisibility, MuteDuration, create_broadcast, create_group, set_muted,
|
||||
},
|
||||
config::Config,
|
||||
constants::*,
|
||||
@@ -138,12 +137,7 @@ mod test_chatlist_events {
|
||||
async fn test_change_chat_visibility() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat_id = create_group_chat(
|
||||
&alice,
|
||||
crate::chat::ProtectionStatus::Unprotected,
|
||||
"my_group",
|
||||
)
|
||||
.await?;
|
||||
let chat_id = create_group(&alice, "my_group").await?;
|
||||
|
||||
chat_id
|
||||
.set_visibility(&alice, ChatVisibility::Pinned)
|
||||
@@ -289,7 +283,7 @@ mod test_chatlist_events {
|
||||
async fn test_delete_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
chat.delete(&alice).await?;
|
||||
@@ -299,11 +293,11 @@ mod test_chatlist_events {
|
||||
|
||||
/// Create group chat
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_group_chat() -> Result<()> {
|
||||
async fn test_create_group() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
alice.evtracker.clear_events();
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
wait_for_chatlist_and_specific_item(&alice, chat).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -324,7 +318,7 @@ mod test_chatlist_events {
|
||||
async fn test_mute_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
chat::set_muted(&alice, chat, MuteDuration::Forever).await?;
|
||||
@@ -343,7 +337,7 @@ mod test_chatlist_events {
|
||||
async fn test_mute_chat_expired() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
|
||||
let mute_duration = MuteDuration::Until(
|
||||
std::time::SystemTime::now()
|
||||
@@ -363,7 +357,7 @@ mod test_chatlist_events {
|
||||
async fn test_change_chat_name() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
chat::set_chat_name(&alice, chat, "New Name").await?;
|
||||
@@ -377,7 +371,7 @@ mod test_chatlist_events {
|
||||
async fn test_change_chat_profile_image() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
let file = alice.dir.path().join("avatar.png");
|
||||
@@ -395,9 +389,7 @@ mod test_chatlist_events {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
|
||||
.await;
|
||||
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
|
||||
|
||||
let sent_msg = alice.send_text(chat, "Hello").await;
|
||||
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
|
||||
@@ -419,9 +411,7 @@ mod test_chatlist_events {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
|
||||
.await;
|
||||
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
|
||||
let sent_msg = alice.send_text(chat, "Hello").await;
|
||||
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
|
||||
|
||||
@@ -438,9 +428,7 @@ mod test_chatlist_events {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
|
||||
.await;
|
||||
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
|
||||
let sent_msg = alice.send_text(chat, "Hello").await;
|
||||
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
|
||||
|
||||
@@ -456,7 +444,7 @@ mod test_chatlist_events {
|
||||
async fn test_delete_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
let message = chat::send_text_msg(&alice, chat, "Hello World".to_owned()).await?;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
@@ -473,9 +461,7 @@ mod test_chatlist_events {
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let chat = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
|
||||
.await;
|
||||
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
|
||||
let sent_msg = alice.send_text(chat, "Hello").await;
|
||||
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
|
||||
chat_id_for_bob.accept(&bob).await?;
|
||||
@@ -516,7 +502,7 @@ mod test_chatlist_events {
|
||||
async fn test_update_after_ephemeral_messages() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
chat.set_ephemeral_timer(&alice, crate::ephemeral::Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
alice
|
||||
@@ -560,8 +546,7 @@ First thread."#;
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let alice_chatid =
|
||||
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
|
||||
let alice_chatid = chat::create_group(&alice.ctx, "the chat").await?;
|
||||
|
||||
// Step 1: Generate QR-code, secure-join implied by chatid
|
||||
let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid)).await?;
|
||||
@@ -608,7 +593,7 @@ First thread."#;
|
||||
async fn test_resend_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
|
||||
let msg_id = chat::send_text_msg(&alice, chat, "Hello".to_owned()).await?;
|
||||
let _ = alice.pop_sent_msg().await;
|
||||
@@ -628,7 +613,7 @@ First thread."#;
|
||||
async fn test_reaction() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
let msg_id = chat::send_text_msg(&alice, chat, "Hello".to_owned()).await?;
|
||||
let _ = alice.pop_sent_msg().await;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user