mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
Compare commits
409 Commits
simon/UICh
...
v1.137.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1ee2b463f | ||
|
|
8df3b1bb1b | ||
|
|
22f240dd4d | ||
|
|
ae10ed5c40 | ||
|
|
aff6bf9402 | ||
|
|
43fc55e542 | ||
|
|
7ea05cb8a0 | ||
|
|
d036ad5853 | ||
|
|
e9280b8413 | ||
|
|
2108a8ba94 | ||
|
|
34f4ec02f6 | ||
|
|
72d5a387fb | ||
|
|
d17d89ea8f | ||
|
|
d2aa76c0ca | ||
|
|
406031773b | ||
|
|
242547f1e9 | ||
|
|
f43f5c6c0f | ||
|
|
910e4bfa37 | ||
|
|
ecf4e651ee | ||
|
|
7b724fa75a | ||
|
|
09776ae71c | ||
|
|
47bea5f8fb | ||
|
|
99cd6d10da | ||
|
|
fae4cb33bc | ||
|
|
7a3be74350 | ||
|
|
20a64ec357 | ||
|
|
92bf48684a | ||
|
|
17701b78d6 | ||
|
|
ff0d506c95 | ||
|
|
8ff3f08c2f | ||
|
|
7a32bcc1f4 | ||
|
|
65822e53e6 | ||
|
|
ac508a9e9c | ||
|
|
225112a8fe | ||
|
|
5d34b225b7 | ||
|
|
6ca6a439bd | ||
|
|
f9465f7512 | ||
|
|
489eae5d66 | ||
|
|
b6c6a63a39 | ||
|
|
c069190b68 | ||
|
|
94ac2b1097 | ||
|
|
6080a52024 | ||
|
|
0aea7d1e02 | ||
|
|
08cbc54c00 | ||
|
|
9731ec419e | ||
|
|
e9cfcd9d1b | ||
|
|
d39cbcdc8d | ||
|
|
fbbefe6b49 | ||
|
|
bab311730c | ||
|
|
b47cad7e68 | ||
|
|
a3b62b9743 | ||
|
|
9aa4c0e56b | ||
|
|
27d2b12e8d | ||
|
|
c1148e4117 | ||
|
|
295f7a291b | ||
|
|
2be28f1311 | ||
|
|
2e42243de8 | ||
|
|
00f2585d8c | ||
|
|
0b73f9cebd | ||
|
|
f5e8a04fd0 | ||
|
|
6721df7d57 | ||
|
|
18d98d643b | ||
|
|
62758658ed | ||
|
|
03bb751a9b | ||
|
|
3ebb1ea95f | ||
|
|
c1d251010f | ||
|
|
7e5959e495 | ||
|
|
823da56f2d | ||
|
|
5bcc44ca9b | ||
|
|
4304e3f0be | ||
|
|
e2e3abdf03 | ||
|
|
dcea188b62 | ||
|
|
5cf725a378 | ||
|
|
2bf0ea9d91 | ||
|
|
1df936aeac | ||
|
|
9ab2c6df16 | ||
|
|
cf11741a8c | ||
|
|
b6a12e3914 | ||
|
|
b753440a68 | ||
|
|
39abc8344c | ||
|
|
65c9e72bf4 | ||
|
|
ea4d954c77 | ||
|
|
43523a96a2 | ||
|
|
2e2fa9e74f | ||
|
|
e43ffb20a1 | ||
|
|
2f0f247e70 | ||
|
|
5bda4f0c26 | ||
|
|
d39c8a3a19 | ||
|
|
e465415039 | ||
|
|
5cef77b8e6 | ||
|
|
60e733c30c | ||
|
|
8b98816eb9 | ||
|
|
50165b3e35 | ||
|
|
0be8b5a5c4 | ||
|
|
451bb6e9db | ||
|
|
83196d4cb5 | ||
|
|
0003e55ad5 | ||
|
|
02014eda6c | ||
|
|
f1c6cd69e9 | ||
|
|
ace281ff6c | ||
|
|
c9edd525e0 | ||
|
|
3f35b442c3 | ||
|
|
87e9365016 | ||
|
|
9806509f4a | ||
|
|
91600a34b6 | ||
|
|
d16351d207 | ||
|
|
4caf638201 | ||
|
|
375fcbd63c | ||
|
|
6ff3a2cf7c | ||
|
|
a890fe3a9a | ||
|
|
2b8bf29fce | ||
|
|
26400a9e4e | ||
|
|
f8b9bb9083 | ||
|
|
42f9047a54 | ||
|
|
6433a3a5f3 | ||
|
|
4b6a03c904 | ||
|
|
ff3df01d98 | ||
|
|
cdc99854b2 | ||
|
|
e7072bcb75 | ||
|
|
7950bde3c6 | ||
|
|
a259669c98 | ||
|
|
603e6be9b4 | ||
|
|
a78c484467 | ||
|
|
e78f07b343 | ||
|
|
8abf10aacb | ||
|
|
2fef4acdd6 | ||
|
|
de27be3a36 | ||
|
|
c62e8539a1 | ||
|
|
22c0aef9c0 | ||
|
|
87805bc36d | ||
|
|
99c4d24eab | ||
|
|
7bf9c4a2d9 | ||
|
|
304e902fce | ||
|
|
0155d93622 | ||
|
|
ebd097bdbe | ||
|
|
a11d01f8a3 | ||
|
|
38491b694b | ||
|
|
e702c1a8ca | ||
|
|
1adea3c678 | ||
|
|
9af812a3e7 | ||
|
|
36bdf8a67e | ||
|
|
20b30fc70a | ||
|
|
e59ff6ca74 | ||
|
|
0e5db36205 | ||
|
|
7960944b14 | ||
|
|
71c2383cbe | ||
|
|
5f5b272726 | ||
|
|
b34fe8f118 | ||
|
|
810be4f6c7 | ||
|
|
1ebbe26ebb | ||
|
|
0f5d5dd2b2 | ||
|
|
473dbe01af | ||
|
|
069ed7afa6 | ||
|
|
9313ece3cd | ||
|
|
900168c68c | ||
|
|
0bd137b4e5 | ||
|
|
75da205ff6 | ||
|
|
67e5fbbfe3 | ||
|
|
570daf42ec | ||
|
|
fcbbb91cde | ||
|
|
c3a7fc4c8d | ||
|
|
4b4c57a480 | ||
|
|
b95d58208c | ||
|
|
c468eb088e | ||
|
|
de37135ed6 | ||
|
|
33777d8759 | ||
|
|
8cc348bfa4 | ||
|
|
76bbd5fd72 | ||
|
|
eaed2381e7 | ||
|
|
6198ed0ef5 | ||
|
|
9f4af679a3 | ||
|
|
e158b889c9 | ||
|
|
9f7defa8da | ||
|
|
e9d7fe0561 | ||
|
|
7d7289bd51 | ||
|
|
ebdc52247c | ||
|
|
36bb4a7a32 | ||
|
|
c0832af634 | ||
|
|
b6db0152b0 | ||
|
|
bc7fd4495b | ||
|
|
e67e86422f | ||
|
|
2030de11d9 | ||
|
|
2c5a0cac5f | ||
|
|
251917e602 | ||
|
|
273719ae7c | ||
|
|
e639b58c6f | ||
|
|
5addfa8d1d | ||
|
|
02d68332c7 | ||
|
|
97abb9a0a9 | ||
|
|
d0e0cfafef | ||
|
|
f630b5fb39 | ||
|
|
d9bab938d5 | ||
|
|
215ec14b20 | ||
|
|
ea728e9b62 | ||
|
|
2af9ff1d01 | ||
|
|
7502234686 | ||
|
|
863a386d0f | ||
|
|
e4b49dfdef | ||
|
|
612aa1431e | ||
|
|
781d3abdb9 | ||
|
|
78d01933ad | ||
|
|
1a1467f7cf | ||
|
|
8d09291d1e | ||
|
|
4ccd2b8d02 | ||
|
|
794596ec69 | ||
|
|
3a787519b3 | ||
|
|
c03e163ed2 | ||
|
|
6cee295a5d | ||
|
|
f0be7daae9 | ||
|
|
0b279ec84e | ||
|
|
e919de78a3 | ||
|
|
6ea675a12f | ||
|
|
b970ebe67a | ||
|
|
3c4c701f9b | ||
|
|
01ac9c8b90 | ||
|
|
f6de23738d | ||
|
|
ddc2704278 | ||
|
|
3d2b164c05 | ||
|
|
2094bc3135 | ||
|
|
acff8205e2 | ||
|
|
255400028a | ||
|
|
d7615b223f | ||
|
|
00fbf540c4 | ||
|
|
288eccf722 | ||
|
|
99ee769580 | ||
|
|
345759d653 | ||
|
|
db0143f01a | ||
|
|
4da0c19766 | ||
|
|
08247a5d37 | ||
|
|
ceadd8928e | ||
|
|
c3d96814ca | ||
|
|
c2953623b9 | ||
|
|
1907d1859e | ||
|
|
a1970e998f | ||
|
|
1e9baefca0 | ||
|
|
e16322d99d | ||
|
|
ecfe3898c6 | ||
|
|
5499ca52bf | ||
|
|
4e8979f7c8 | ||
|
|
417db31098 | ||
|
|
cd9f6c3d5b | ||
|
|
07870a6d69 | ||
|
|
b08a4d6fcf | ||
|
|
b3a82b416f | ||
|
|
4e5d7fb821 | ||
|
|
1d73f97ef3 | ||
|
|
f5601e7683 | ||
|
|
0000c09ad3 | ||
|
|
a83884d7e9 | ||
|
|
9e00e8627f | ||
|
|
85c9622675 | ||
|
|
30432d8fa5 | ||
|
|
8b9f19be70 | ||
|
|
39c317e211 | ||
|
|
36ab7bdf47 | ||
|
|
f8f0ca08da | ||
|
|
2a0a05d03c | ||
|
|
7bc2f0cb6b | ||
|
|
4355bd77a9 | ||
|
|
f0091696c2 | ||
|
|
d2e86c5852 | ||
|
|
d4a505b52e | ||
|
|
08a30031eb | ||
|
|
44686d6caa | ||
|
|
9862d40f89 | ||
|
|
256c8c13f1 | ||
|
|
0b3a56c3c4 | ||
|
|
89024bbf37 | ||
|
|
cf16671d8d | ||
|
|
671feb68a4 | ||
|
|
ccd5158109 | ||
|
|
0a18e32d62 | ||
|
|
e9fadc0785 | ||
|
|
cfa13f0669 | ||
|
|
89e43c6678 | ||
|
|
8a67797cb1 | ||
|
|
b29bc19ef4 | ||
|
|
e765066f05 | ||
|
|
67aa785a9e | ||
|
|
c88c26426d | ||
|
|
b4e9a9764f | ||
|
|
06e79e8926 | ||
|
|
9427f7b587 | ||
|
|
bce22edfe3 | ||
|
|
656d4ed506 | ||
|
|
5e3fcafb3a | ||
|
|
660cfd4f01 | ||
|
|
7a1270f861 | ||
|
|
b35b893351 | ||
|
|
f45f9263db | ||
|
|
8289dc92ed | ||
|
|
862107c708 | ||
|
|
778660a8c9 | ||
|
|
6e55f0c6e3 | ||
|
|
3b0e740c17 | ||
|
|
2dd87b6b5e | ||
|
|
cdcacf2f83 | ||
|
|
51aaaf2e8d | ||
|
|
e6438f9981 | ||
|
|
9135cffaa4 | ||
|
|
73492ca4bc | ||
|
|
fe3c1f69c3 | ||
|
|
31ee3feb57 | ||
|
|
f4ed63c54c | ||
|
|
8f88cdd826 | ||
|
|
9933a4268f | ||
|
|
8a54c228fd | ||
|
|
b5f2c747e0 | ||
|
|
ba35e83db2 | ||
|
|
61a2c551fc | ||
|
|
20c91ba2fa | ||
|
|
969f8b916b | ||
|
|
b7b7a7e95d | ||
|
|
455b108a6c | ||
|
|
645ca7741b | ||
|
|
36643c551d | ||
|
|
0fcdee8857 | ||
|
|
26ae686687 | ||
|
|
b94bd9a659 | ||
|
|
f15e7d43e3 | ||
|
|
05c256dd5b | ||
|
|
37295f6967 | ||
|
|
dfdbb91f0a | ||
|
|
72f93dca7a | ||
|
|
ec2cf31cfa | ||
|
|
ecd4d2afe0 | ||
|
|
ec9d104cf3 | ||
|
|
11214c7d1f | ||
|
|
fba27ff884 | ||
|
|
f8907e3c83 | ||
|
|
f1688d2b3f | ||
|
|
693045b542 | ||
|
|
14dfb9abec | ||
|
|
c8ed3ed73b | ||
|
|
bce5203eeb | ||
|
|
74c0c2cc38 | ||
|
|
4f25072352 | ||
|
|
91c3a39134 | ||
|
|
ae94b2a7b3 | ||
|
|
3b013a1017 | ||
|
|
80aab220b6 | ||
|
|
34c3e44b9d | ||
|
|
78d304443a | ||
|
|
d6c24eb9f6 | ||
|
|
f7fd1ef2bf | ||
|
|
af7bf5bd2b | ||
|
|
ea666f1098 | ||
|
|
5bb80f94c7 | ||
|
|
2f29c56a36 | ||
|
|
de86b8a96e | ||
|
|
060c9c8aa1 | ||
|
|
727428a965 | ||
|
|
df455bbcf5 | ||
|
|
946eea4c9e | ||
|
|
5cbc87369e | ||
|
|
5cdd5e0564 | ||
|
|
f493d6bb40 | ||
|
|
8e073b9c3e | ||
|
|
ea2a692d18 | ||
|
|
1b7c5be9c5 | ||
|
|
f7903df805 | ||
|
|
d2c61dc90e | ||
|
|
7b68098785 | ||
|
|
48f2ea717e | ||
|
|
cb3f03fd39 | ||
|
|
06f1fe18d6 | ||
|
|
1dbf924c6a | ||
|
|
3f6814f421 | ||
|
|
782828ac4f | ||
|
|
bd3759d55e | ||
|
|
672993e69e | ||
|
|
987bdaf237 | ||
|
|
7cf382a3b8 | ||
|
|
19dce9ddfa | ||
|
|
0afc0dd65a | ||
|
|
73d612a07d | ||
|
|
3b1529ef81 | ||
|
|
15187c0adb | ||
|
|
c5f31c3d03 | ||
|
|
2c17e78347 | ||
|
|
4ee646ce0b | ||
|
|
1f7b4a74fa | ||
|
|
4bc90701cc | ||
|
|
490deb9347 | ||
|
|
28d9484a13 | ||
|
|
e67e684ee0 | ||
|
|
6cfe3e6a97 | ||
|
|
99ac524905 | ||
|
|
2faf7fdb78 | ||
|
|
6a8ea8a083 | ||
|
|
e0e56cd831 | ||
|
|
bbc6febb72 | ||
|
|
7f7f42d721 | ||
|
|
589236c27b | ||
|
|
c16c5e0802 | ||
|
|
36cab40ac1 | ||
|
|
4186d78305 | ||
|
|
06cccb77f8 | ||
|
|
1895f4c556 | ||
|
|
849a873e61 | ||
|
|
b5c0372c99 | ||
|
|
1ba9b69849 | ||
|
|
6345a4f5b3 | ||
|
|
382fc75b1e | ||
|
|
92fc9ea971 | ||
|
|
de7ac2a240 | ||
|
|
7b0e5adaee | ||
|
|
406b59501b | ||
|
|
d5da2bed75 |
78
.github/workflows/ci.yml
vendored
78
.github/workflows/ci.yml
vendored
@@ -24,9 +24,11 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.75.0
|
||||
RUSTUP_TOOLCHAIN: 1.77.1
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Install rustfmt and clippy
|
||||
run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy
|
||||
- name: Cache rust cargo artifacts
|
||||
@@ -42,7 +44,9 @@ jobs:
|
||||
name: cargo deny
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
with:
|
||||
arguments: --all-features --workspace
|
||||
@@ -53,7 +57,9 @@ jobs:
|
||||
name: Check provider database
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Check provider database
|
||||
run: scripts/update-provider-database.sh
|
||||
|
||||
@@ -63,8 +69,9 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
- name: Rustdoc
|
||||
@@ -76,18 +83,20 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.75.0
|
||||
rust: 1.77.1
|
||||
- os: windows-latest
|
||||
rust: 1.75.0
|
||||
rust: 1.77.1
|
||||
- os: macos-latest
|
||||
rust: 1.75.0
|
||||
rust: 1.77.1
|
||||
|
||||
# Minimum Supported Rust Version = 1.70.0
|
||||
# Minimum Supported Rust Version = 1.77.0
|
||||
- os: ubuntu-latest
|
||||
rust: 1.70.0
|
||||
rust: 1.77.0
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Install Rust ${{ matrix.rust }}
|
||||
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
|
||||
@@ -96,10 +105,20 @@ jobs:
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo test --workspace
|
||||
run: cargo nextest run --workspace
|
||||
|
||||
- name: Doc-Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo test --workspace --doc
|
||||
|
||||
- name: Test cargo vendor
|
||||
run: cargo vendor
|
||||
@@ -111,7 +130,9 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
@@ -120,7 +141,7 @@ jobs:
|
||||
run: cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
- name: Upload C library
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug/libdeltachat.a
|
||||
@@ -133,7 +154,9 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
@@ -142,7 +165,7 @@ jobs:
|
||||
run: cargo build -p deltachat-rpc-server
|
||||
|
||||
- name: Upload deltachat-rpc-server
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
||||
@@ -152,8 +175,9 @@ jobs:
|
||||
name: Python lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
@@ -193,16 +217,18 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Download libdeltachat.a
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -243,10 +269,12 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -254,7 +282,7 @@ jobs:
|
||||
run: pip install tox
|
||||
|
||||
- name: Download deltachat-rpc-server
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: target/debug
|
||||
|
||||
228
.github/workflows/deltachat-rpc-server.yml
vendored
228
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -21,69 +21,65 @@ jobs:
|
||||
# Build a version statically linked against musl libc
|
||||
# to avoid problems with glibc version incompatibility.
|
||||
build_linux:
|
||||
name: Cross-compile deltachat-rpc-server for x86_64, i686, aarch64 and armv7 Linux
|
||||
runs-on: ubuntu-22.04
|
||||
name: Linux
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install ziglang
|
||||
run: pip install wheel ziglang==0.11.0
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: sh scripts/zig-rpc-server.sh
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
|
||||
- name: Upload dist directory with Linux binaries
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux
|
||||
path: dist/
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
path: result/bin/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
build_windows:
|
||||
name: Build deltachat-rpc-server for Windows
|
||||
name: Windows
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
artifact: win32.exe
|
||||
path: deltachat-rpc-server.exe
|
||||
target: i686-pc-windows-msvc
|
||||
|
||||
- os: windows-latest
|
||||
artifact: win64.exe
|
||||
path: deltachat-rpc-server.exe
|
||||
target: x86_64-pc-windows-msvc
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
arch: [win32, win64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.target }} --features vendored
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.artifact }}
|
||||
path: target/${{ matrix.target}}/release/${{ matrix.path }}
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}
|
||||
path: result/bin/deltachat-rpc-server.exe
|
||||
if-no-files-found: error
|
||||
|
||||
build_macos:
|
||||
name: Build deltachat-rpc-server for macOS
|
||||
name: macOS
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
- arch: aarch64
|
||||
arch: [x86_64, aarch64]
|
||||
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.arch }}-apple-darwin
|
||||
@@ -92,61 +88,140 @@ jobs:
|
||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
||||
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
build_android:
|
||||
name: Android
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [arm64-v8a, armeabi-v7a]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
path: result/bin/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Build wheels and upload binaries to the release
|
||||
needs: ["build_linux", "build_windows", "build_macos"]
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/deltachat-rpc-server
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download Linux binaries
|
||||
uses: actions/download-artifact@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
name: linux
|
||||
path: dist/
|
||||
show-progress: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
|
||||
- name: Download win32 binary
|
||||
uses: actions/download-artifact@v3
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-win32.exe
|
||||
path: deltachat-rpc-server-win32.exe.d
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
|
||||
- name: Download win64 binary
|
||||
uses: actions/download-artifact@v3
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-win64.exe
|
||||
path: deltachat-rpc-server-win64.exe.d
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-macos
|
||||
path: deltachat-rpc-server-x86_64-macos.d
|
||||
|
||||
- name: Download macOS binary for aarch64
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-macos
|
||||
path: deltachat-rpc-server-aarch64-macos.d
|
||||
|
||||
- name: Flatten dist/ directory
|
||||
- name: Download Android binary for arm64-v8a
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-arm64-v8a-android
|
||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||
|
||||
- name: Download Android binary for armeabi-v7a
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android
|
||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||
|
||||
- name: Create bin/ directory
|
||||
run: |
|
||||
mv deltachat-rpc-server-win32.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win32.exe
|
||||
mv deltachat-rpc-server-win64.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win64.exe
|
||||
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-macos
|
||||
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-macos
|
||||
mkdir -p bin
|
||||
mv deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-linux
|
||||
mv deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv7l-linux
|
||||
mv deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv6l-linux
|
||||
mv deltachat-rpc-server-i686-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-i686-linux
|
||||
mv deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-linux
|
||||
mv deltachat-rpc-server-win32.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win32.exe
|
||||
mv deltachat-rpc-server-win64.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win64.exe
|
||||
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-macos
|
||||
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-macos
|
||||
mv deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-arm64-v8a-android
|
||||
mv deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-armeabi-v7a-android
|
||||
|
||||
- name: List binaries
|
||||
run: ls -l bin/
|
||||
|
||||
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
|
||||
- name: Install python 3.12
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
@@ -154,15 +229,40 @@ jobs:
|
||||
run: pip install wheel
|
||||
|
||||
- name: Build deltachat-rpc-server Python wheels and source package
|
||||
run: scripts/wheel-rpc-server.py
|
||||
run: |
|
||||
mkdir -p dist
|
||||
nix build .#deltachat-rpc-server-x86_64-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-armv7l-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-armv6l-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-aarch64-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-i686-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-win64-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-win32-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-source
|
||||
cp result/*.tar.gz dist/
|
||||
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
|
||||
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
|
||||
mv *.whl dist/
|
||||
|
||||
- name: List downloaded artifacts
|
||||
- name: List artifacts
|
||||
run: ls -l dist/
|
||||
|
||||
- name: Upload binaries to the GitHub release
|
||||
if: github.event_name == 'release'
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
run: |
|
||||
gh release upload ${{ github.ref_name }} \
|
||||
--repo ${{ github.repository }} \
|
||||
dist/*
|
||||
bin/* dist/*
|
||||
|
||||
- name: Publish deltachat-rpc-client to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
||||
@@ -13,9 +13,10 @@ jobs:
|
||||
steps:
|
||||
- name: Install tree
|
||||
run: sudo apt install tree
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: Get tag
|
||||
@@ -49,7 +50,7 @@ jobs:
|
||||
ls -lah
|
||||
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deltachat-jsonrpc-client.tgz
|
||||
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
||||
|
||||
6
.github/workflows/jsonrpc.yml
vendored
6
.github/workflows/jsonrpc.yml
vendored
@@ -14,9 +14,11 @@ jobs:
|
||||
build_and_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Add Rust cache
|
||||
|
||||
6
.github/workflows/node-docs.yml
vendored
6
.github/workflows/node-docs.yml
vendored
@@ -14,10 +14,12 @@ jobs:
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
|
||||
49
.github/workflows/node-package.yml
vendored
49
.github/workflows/node-package.yml
vendored
@@ -14,9 +14,10 @@ jobs:
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: System info
|
||||
@@ -28,7 +29,7 @@ jobs:
|
||||
node --version
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ env.APPDATA }}/npm-cache
|
||||
@@ -36,7 +37,7 @@ jobs:
|
||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/
|
||||
@@ -56,7 +57,7 @@ jobs:
|
||||
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
||||
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: node/${{ matrix.os }}.tar.gz
|
||||
@@ -74,9 +75,10 @@ jobs:
|
||||
- name: Change working directory owner
|
||||
run: chown root:root .
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
- run: apt-get update
|
||||
@@ -97,7 +99,7 @@ jobs:
|
||||
node --version
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ env.APPDATA }}/npm-cache
|
||||
@@ -105,7 +107,7 @@ jobs:
|
||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/
|
||||
@@ -125,7 +127,7 @@ jobs:
|
||||
tar -zcvf "linux.tar.gz" -C prebuilds .
|
||||
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux
|
||||
path: node/linux.tar.gz
|
||||
@@ -137,9 +139,10 @@ jobs:
|
||||
steps:
|
||||
- name: Install tree
|
||||
run: sudo apt install tree
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: Get tag
|
||||
@@ -165,25 +168,25 @@ jobs:
|
||||
node --version
|
||||
echo $DELTACHAT_NODE_TAR_GZ
|
||||
- name: Download Linux prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: linux
|
||||
- name: Download macOS prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: macos-latest
|
||||
- name: Download Windows prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows-latest
|
||||
- shell: bash
|
||||
run: |
|
||||
mkdir node/prebuilds
|
||||
tar -xvzf linux/linux.tar.gz -C node/prebuilds
|
||||
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
|
||||
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
|
||||
tar -xvzf linux.tar.gz -C node/prebuilds
|
||||
tar -xvzf macos-latest.tar.gz -C node/prebuilds
|
||||
tar -xvzf windows-latest.tar.gz -C node/prebuilds
|
||||
tree node/prebuilds
|
||||
rm -rf linux macos-latest windows-latest
|
||||
rm -f linux.tar.gz macos-latest.tar.gz windows-latest.tar.gz
|
||||
- name: Install dependencies without running scripts
|
||||
run: |
|
||||
npm install --ignore-scripts
|
||||
@@ -201,7 +204,7 @@ jobs:
|
||||
ls -lah
|
||||
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
|
||||
- name: Upload prebuild
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deltachat-node.tgz
|
||||
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}
|
||||
|
||||
11
.github/workflows/node-tests.yml
vendored
11
.github/workflows/node-tests.yml
vendored
@@ -23,9 +23,10 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: System info
|
||||
@@ -37,7 +38,7 @@ jobs:
|
||||
node --version
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ env.APPDATA }}/npm-cache
|
||||
@@ -45,7 +46,7 @@ jobs:
|
||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/
|
||||
|
||||
47
.github/workflows/publish-deltachat-rpc-client-pypi.yml
vendored
Normal file
47
.github/workflows/publish-deltachat-rpc-client-pypi.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Publish deltachat-rpc-client to PyPI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build distribution
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Install pypa/build
|
||||
run: python3 -m pip install build
|
||||
- name: Build a binary wheel and a source tarball
|
||||
working-directory: deltachat-rpc-client
|
||||
run: python3 -m build
|
||||
- name: Store the distribution packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: deltachat-rpc-client/dist/
|
||||
|
||||
publish-to-pypi:
|
||||
name: Publish Python distribution to PyPI
|
||||
if: github.event_name == 'release'
|
||||
needs:
|
||||
- build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/deltachat-rpc-client
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish deltachat-rpc-client to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
16
.github/workflows/repl.yml
vendored
16
.github/workflows/repl.yml
vendored
@@ -10,15 +10,17 @@ on:
|
||||
jobs:
|
||||
build_repl:
|
||||
name: Build REPL example
|
||||
runs-on: windows-latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- name: Build
|
||||
run: cargo build -p deltachat-repl --features vendored
|
||||
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: repl.exe
|
||||
path: "target/debug/deltachat-repl.exe"
|
||||
path: "result/bin/deltachat-repl.exe"
|
||||
|
||||
46
.github/workflows/upload-docs.yml
vendored
46
.github/workflows/upload-docs.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build & Deploy Documentation on rs.delta.chat
|
||||
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -6,11 +6,13 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-rs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
cargo doc --package deltachat --no-deps --document-private-items
|
||||
@@ -22,3 +24,41 @@ jobs:
|
||||
HOST: "delta.chat"
|
||||
SOURCE: "target/doc"
|
||||
TARGET: "/var/www/html/rs/"
|
||||
|
||||
build-python:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@py.delta.chat:/home/delta/build/master"
|
||||
|
||||
build-c:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
|
||||
|
||||
18
.github/workflows/upload-ffi-docs.yml
vendored
18
.github/workflows/upload-ffi-docs.yml
vendored
@@ -1,5 +1,5 @@
|
||||
# GitHub Actions workflow
|
||||
# to build `deltachat_fii` crate documentation
|
||||
# to build `deltachat_ffi` crate documentation
|
||||
# and upload it to <https://cffi.delta.chat/>
|
||||
|
||||
name: Build & Deploy Documentation on cffi.delta.chat
|
||||
@@ -14,15 +14,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
cargo doc --package deltachat_ffi --no-deps
|
||||
- name: Upload to cffi.delta.chat
|
||||
uses: up9cloud/action-rsync@v1.3
|
||||
env:
|
||||
USER: ${{ secrets.USERNAME }}
|
||||
KEY: ${{ secrets.KEY }}
|
||||
HOST: "delta.chat"
|
||||
SOURCE: "target/doc"
|
||||
TARGET: "/var/www/html/cffi/"
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.USERNAME }}@delta.chat:/var/www/html/cffi/"
|
||||
|
||||
25
.github/workflows/upload-python-docs.yml
vendored
25
.github/workflows/upload-python-docs.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: Build & Deploy Documentation on py.delta.chat
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- name: Build Python documentation
|
||||
run: scripts/build-python-docs.sh
|
||||
- name: Upload to py.delta.chat
|
||||
uses: up9cloud/action-rsync@v1.3
|
||||
env:
|
||||
USER: delta
|
||||
KEY: ${{ secrets.CODESPEAK_KEY }}
|
||||
HOST: "lists.codespeak.net"
|
||||
SOURCE: "dist/html/"
|
||||
TARGET: "/home/delta/build/master"
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -44,3 +44,10 @@ node/build/
|
||||
node/dist/
|
||||
node/prebuilds/
|
||||
node/.nyc_output/
|
||||
|
||||
# Nix symlink.
|
||||
result
|
||||
|
||||
# direnv
|
||||
.envrc
|
||||
.direnv
|
||||
626
CHANGELOG.md
626
CHANGELOG.md
@@ -1,5 +1,613 @@
|
||||
# Changelog
|
||||
|
||||
## [1.137.4] - 2024-04-24
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove `Stream` implementation for `EventEmitter`.
|
||||
- Experimental Webxdc Integration API, Maps Integration ([#5461](https://github.com/deltachat/deltachat-core-rust/pull/5461)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add progressive backoff for failing IMAP connection attempts ([#5443](https://github.com/deltachat/deltachat-core-rust/pull/5443)).
|
||||
- Replace event channel with broadcast channel.
|
||||
- Mark contact request messages as seen on IMAP.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Convert images to RGB8 (without alpha) before encoding into JPEG to fix sending of large RGBA images.
|
||||
- Don't set `is_bot` for webxdc status updates ([#5445](https://github.com/deltachat/deltachat-core-rust/pull/5445)).
|
||||
- Do not fail if Autocrypt Setup Message has no encryption preference to fix key transfer from K-9 Mail to Delta Chat.
|
||||
- Use only CRLF in Autocrypt Setup Message.
|
||||
- python: Use cached message object if `dc_get_msg()` returns `NULL`.
|
||||
- python: `Message::is_outgoing`: Don't reload message from db.
|
||||
- python: `_map_ffi_event`: Always check if `get_message_by_id()` returned None.
|
||||
- node: Undefine `NAPI_EXPERIMENTAL` to fix build with new clang.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: Add `imap-tools` as `deltachat-rpc-client` dependency.
|
||||
- nix: Add `./deltachat-contact-tools` to sources.
|
||||
- nix: Update nix flake.
|
||||
- deps: Update rustls to 0.21.11.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update references to SecureJoin protocols.
|
||||
- Fix broken references in documentation comments.
|
||||
|
||||
### Refactor
|
||||
|
||||
- imap: remove `RwLock` from `ratelimit`.
|
||||
- deltachat-ffi: Remove unused `ResultNullableExt`.
|
||||
- Remove duplicate clippy exceptions.
|
||||
- Group `use` at the top of the test modules.
|
||||
|
||||
## [1.137.3] - 2024-04-16
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove reactions ffi; all implementations use jsonrpc.
|
||||
- Don't load trashed messages with `Message::load_from_db`.
|
||||
- Add `ChatListChanged` and `ChatListItemChanged` events ([#4476](https://github.com/deltachat/deltachat-core-rust/pull/4476)).
|
||||
- deltachat-rpc-client: Add `check_qr` and `set_config_from_qr` APIs.
|
||||
- deltachat-rpc-client: Add `Account.create_chat()`.
|
||||
- deltachat-rpc-client: Add `Message.wait_until_delivered()`.
|
||||
- deltachat-rpc-client: Add `Chat.send_file()`.
|
||||
- deltachat-rpc-client: Add `Account.wait_for_reactions_changed()`.
|
||||
- deltachat-rpc-client: Return Message from `Message.send_reaction()`.
|
||||
- deltachat-rpc-client: Add `Account.bring_online()`.
|
||||
- deltachat-rpc-client: Add `ACFactory.get_accepted_chat()`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Port `direct_imap.py` into deltachat-rpc-client.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not emit `MSGS_CHANGED` event for outgoing hidden messages.
|
||||
- `Message::get_summary()` must not return reaction summary.
|
||||
- Fix emitting `ContactsChanged` events on "recently seen" status change ([#5377](https://github.com/deltachat/deltachat-core-rust/pull/5377)).
|
||||
- deltachat-jsonrpc: block in `inner_get_backup_qr`.
|
||||
- Add tolerance to `MemberListTimestamp` ([#5366](https://github.com/deltachat/deltachat-core-rust/pull/5366)).
|
||||
- Keep webxdc instance for `delete_device_after` period after a status update ([#5365](https://github.com/deltachat/deltachat-core-rust/pull/5365)).
|
||||
- Don't try to do `fetch_move_delete()` if Trash is needed but not yet configured.
|
||||
- Assign messages to chats based on not fully downloaded references.
|
||||
- Do not create ad-hoc groups from partial downloads.
|
||||
- deltachat-rpc-client: construct Thread with `target` keyword argument.
|
||||
- Format error context in `Message::load_from_db`.
|
||||
|
||||
### Build system
|
||||
|
||||
- cmake: adapt target install path if env var `CARGO_BUILD_TARGET` is set.
|
||||
- nix: Use stable Rust in flake.nix devshell.
|
||||
|
||||
### CI
|
||||
|
||||
- Use cargo-nextest instead of cargo-test.
|
||||
- Run doc tests with cargo test --workspace --doc ([#5459](https://github.com/deltachat/deltachat-core-rust/pull/5459)).
|
||||
- Typos in CI files ([#5453](https://github.com/deltachat/deltachat-core-rust/pull/5453)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add <https://deps.rs> badge.
|
||||
- Add 'Ubuntu Touch' to the list of 'frontend projects'
|
||||
|
||||
### Refactor
|
||||
|
||||
- Do not ignore `Contact::get_by_id` errors in `get_encrinfo`.
|
||||
- deltachat-rpc-client: Use `list`, `set` and `tuple` instead of `typing`.
|
||||
- Use `clone_from()` ([#5451](https://github.com/deltachat/deltachat-core-rust/pull/5451)).
|
||||
- Do not check for `is_trash()` in `get_last_reaction_if_newer_than()`.
|
||||
- Split off functional contact tools into its own crate ([#5444](https://github.com/deltachat/deltachat-core-rust/pull/5444))
|
||||
- Fix nightly clippy warnings.
|
||||
|
||||
### Tests
|
||||
|
||||
- Test withdrawing group join QR codes.
|
||||
- `display_chat()`: Don't add day markers.
|
||||
- Move reaction tests to JSON-RPC.
|
||||
- node: Increase 'static tests' timeout to 5 minutes.
|
||||
|
||||
## [1.137.2] - 2024-04-05
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Increase Minimum Supported Rust Version to 1.77.0.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Show reactions in summaries ([#5387](https://github.com/deltachat/deltachat-core-rust/pull/5387)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Test reactions for forwarded messages
|
||||
|
||||
### Refactor
|
||||
|
||||
- `is_probably_private_reply`: Remove reaction-specific code.
|
||||
- Use Rust 1.77.0 support for recursion in async functions.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump rustyline from 13.0.0 to 14.0.0.
|
||||
- Update chrono from 0.4.34 to 0.4.37.
|
||||
- Update from brotli 3.4.0 to brotli 4.0.0.
|
||||
- Upgrade `h2` from 0.4.3 to 0.4.4.
|
||||
- Upgrade `image` from 0.24.9 to 0.25.1.
|
||||
- cargo: Bump fast-socks5 from 0.9.5 to 0.9.6.
|
||||
|
||||
## [1.137.1] - 2024-04-03
|
||||
|
||||
### CI
|
||||
|
||||
- Remove android builds for `x86` and `x86_64`.
|
||||
|
||||
## [1.137.0] - 2024-04-02
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove data from `DC_EVENT_INCOMING_MSG_BUNCH`.
|
||||
- [**breaking**] Remove unused `dc_accounts_all_work_done()` ([#5384](https://github.com/deltachat/deltachat-core-rust/pull/5384)).
|
||||
- deltachat-rpc-client: Add futures.
|
||||
|
||||
### Build system
|
||||
|
||||
- cmake: Build outside the source tree.
|
||||
- nix: Add outputs for Android binaries.
|
||||
- Add `repository` to Cargo.toml.
|
||||
- python: Remove `setuptools_scm` dependency.
|
||||
- Add development shell ([#5390](https://github.com/deltachat/deltachat-core-rust/pull/5390)).
|
||||
|
||||
### CI
|
||||
|
||||
- Update to Rust 1.77.0.
|
||||
- Build deltachat-rpc-server for Android.
|
||||
- Shorter names for deltachat-rpc-server jobs.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Do not include provider hostname in `Message-ID`.
|
||||
- Include 3 recent Message-IDs in `References` header.
|
||||
- Include more entries into DNS fallback cache.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Preserve upper-/lowercase of links parsed by `dehtml()` ([#5362](https://github.com/deltachat/deltachat-core-rust/pull/5362)).
|
||||
- Rescan folders after changing `Config::SentboxWatch`.
|
||||
- Do not ignore `Contact::get_by_id()` error in `from_field_to_contact_id()`.
|
||||
- Put overridden sender name into message info.
|
||||
- Don't send selfavatar in `SecureJoin` messages before contact verification ([#5354](https://github.com/deltachat/deltachat-core-rust/pull/5354)).
|
||||
- Always set correct `chat_id` for `DC_EVENT_REACTIONS_CHANGED` ([#5419](https://github.com/deltachat/deltachat-core-rust/pull/5419)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove `MessageObject::from_message_id()`.
|
||||
- jsonrpc: Add `msg_id` and `account_id` to `get_message()` errors.
|
||||
- Cleanup `jobs` and `Params` relicts.
|
||||
|
||||
### Tests
|
||||
|
||||
- `Test_mvbox_sentbox_threads`: Check that sentbox gets configured after setting `sentbox_watch` ([#5105](https://github.com/deltachat/deltachat-core-rust/pull/5105)).
|
||||
- Remove flaky time check from `test_list_from()`.
|
||||
- Add failing test for #5418 (wrong `DC_EVENT_REACTIONS_CHANGED`)
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Add `result` to .gitignore.
|
||||
- cargo: Bump thiserror from 1.0.57 to 1.0.58.
|
||||
- cargo: Bump tokio from 1.36.0 to 1.37.0.
|
||||
- cargo: Bump pin-project from 1.1.4 to 1.1.5.
|
||||
- cargo: Bump strum from 0.26.1 to 0.26.2.
|
||||
- cargo: Bump uuid from 1.7.0 to 1.8.0.
|
||||
- cargo: Bump toml from 0.8.10 to 0.8.12.
|
||||
- cargo: Bump tokio-stream from 0.1.14 to 0.1.15.
|
||||
- cargo: Bump smallvec from 1.13.1 to 1.13.2.
|
||||
- cargo: Bump async-smtp from 0.9.0 to 0.9.1.
|
||||
- cargo: Bump strum_macros from 0.26.1 to 0.26.2.
|
||||
- cargo: Bump serde_json from 1.0.114 to 1.0.115.
|
||||
- cargo: Bump anyhow from 1.0.80 to 1.0.81.
|
||||
- cargo: Bump syn from 2.0.52 to 2.0.57.
|
||||
- cargo: Bump futures-lite from 2.2.0 to 2.3.0.
|
||||
- cargo: Bump axum from 0.7.4 to 0.7.5.
|
||||
- cargo: Bump reqwest from 0.11.24 to 0.12.2.
|
||||
- cargo: Bump backtrace from 0.3.69 to 0.3.71.
|
||||
- cargo: Bump regex from 1.10.3 to 1.10.4.
|
||||
- cargo: Update aho-corasick from 1.1.2 to 1.1.3.
|
||||
- Update deny.toml.
|
||||
|
||||
## [1.136.6] - 2024-03-19
|
||||
|
||||
### Build system
|
||||
|
||||
- Add description to deltachat-rpc-server wheels.
|
||||
- Read version from Cargo.toml in wheel-rpc-server.py.
|
||||
|
||||
### CI
|
||||
|
||||
- Update actions/cache from v3 to v4.
|
||||
- Automate publishing of deltachat-rpc-server to PyPI.
|
||||
|
||||
### Documentation
|
||||
|
||||
- deltachat-rpc-server: Update deltachat-rpc-client URL.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Nix flake update.
|
||||
|
||||
## [1.136.5] - 2024-03-18
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Nicer summaries: prefer emoji over names
|
||||
- Add `save_mime_headers` to debug info ([#5350](https://github.com/deltachat/deltachat-core-rust/pull/5350))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Terminate ephemeral and location loop immediately on channel close.
|
||||
- Update MemberListTimestamp when sending a group message.
|
||||
- On iOS, use FILE (default) instead of MEMORY ([#5349](https://github.com/deltachat/deltachat-core-rust/pull/5349)).
|
||||
- Add white background to recoded avatars ([#3787](https://github.com/deltachat/deltachat-core-rust/pull/3787)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Add README to deltachat-rpc-client Python packages.
|
||||
|
||||
### Documentation
|
||||
|
||||
- deltachat-rpc-client: Document that 0 is a special value of `set_ephemeral_timer()`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Test that reordering of Member added message results in square bracket error.
|
||||
|
||||
## [1.136.4] - 2024-03-11
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: Make .#libdeltachat buildable on macOS.
|
||||
- Build deltachat-rpc-server wheels with nix.
|
||||
|
||||
### CI
|
||||
|
||||
- Add workflow for automatic publishing of deltachat-rpc-client.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Remove duplicate CHANGELOG entries for 1.135.1.
|
||||
|
||||
## [1.136.3] - 2024-03-09
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Start IMAP loop for sentbox only if it is configured ([#5105](https://github.com/deltachat/deltachat-core-rust/pull/5105)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Remove leading whitespace from Subject ([#5106](https://github.com/deltachat/deltachat-core-rust/pull/5106)).
|
||||
- Create new Peerstate for unencrypted message with already known Autocrypt key, but a new address.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: Cleanup cross-compilation code.
|
||||
- nix: Include SystemConfiguration framework on darwin systems.
|
||||
|
||||
### CI
|
||||
|
||||
- Wait for `build_windows` task before trying to publish it.
|
||||
- Remove artifacts from npm package.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Don't parse Autocrypt header for outgoing messages ([#5259](https://github.com/deltachat/deltachat-core-rust/pull/5259)).
|
||||
- Remove `deduplicate_peerstates()`.
|
||||
- Fix 2024-03-05 nightly clippy warnings.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- deps: Bump mio from 0.8.8 to 0.8.11 in /fuzz.
|
||||
- RPC client: Add missing constants ([#5110](https://github.com/deltachat/deltachat-core-rust/pull/5110)).
|
||||
|
||||
## [1.136.2] - 2024-03-05
|
||||
|
||||
### Build system
|
||||
|
||||
- Downgrade `cc` to 1.0.83 to fix build for Android.
|
||||
|
||||
### CI
|
||||
|
||||
- Update setup-node action.
|
||||
|
||||
## [1.136.1] - 2024-03-05
|
||||
|
||||
### Build system
|
||||
|
||||
- Revert to OpenSSL 3.1.
|
||||
- Restore MSRV 1.70.0.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update node constants.
|
||||
|
||||
## [1.136.0] - 2024-03-04
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Recognise Trash folder by name ([#5275](https://github.com/deltachat/deltachat-core-rust/pull/5275)).
|
||||
- Send Chat-Group-Avatar as inline base64 ([#5253](https://github.com/deltachat/deltachat-core-rust/pull/5253)).
|
||||
- Self-Reporting: Report number of protected/encrypted/unencrypted chats ([#5292](https://github.com/deltachat/deltachat-core-rust/pull/5292)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't send sync messages on self-{status,avatar} update from self-sent messages ([#5289](https://github.com/deltachat/deltachat-core-rust/pull/5289)).
|
||||
- imap: Allow `maybe_network` to interrupt connection ratelimit.
|
||||
- imap: Set connectivity to "connecting" only after ratelimit.
|
||||
- Remove `Group-ID` from `Message-ID`.
|
||||
- Prioritize protected `Message-ID` over `X-Microsoft-Original-Message-ID`.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Make `store_self_keypair` private.
|
||||
- Add `ContextBuilder.build()` to build Context without opening.
|
||||
- `dc_accounts_set_push_device_token` and `dc_get_push_state` APIs for iOS push notifications.
|
||||
|
||||
### Build system
|
||||
|
||||
- Tag armv6 wheels with tags accepted by PyPI.
|
||||
- Unpin OpenSSL.
|
||||
- Remove deprecated `unmaintained` field from deny.toml.
|
||||
- Do not vendor OpenSSL when cross-compiling ([#5316](https://github.com/deltachat/deltachat-core-rust/pull/5316)).
|
||||
- Increase MSRV to 1.74.0.
|
||||
|
||||
### CI
|
||||
|
||||
- Upgrade setup-python GitHub Action.
|
||||
- Update to Rust 1.76 and fix clippy warnings.
|
||||
- Build Python docs with Nix.
|
||||
- Upload python docs without GH actions.
|
||||
- Upload cffi docs without GH actions.
|
||||
- Build c.delta.chat docs with nix.
|
||||
|
||||
### Other
|
||||
|
||||
- refactor: move more methods from Imap into Session.
|
||||
- Add deltachat-time to sources.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove Session from Imap structure.
|
||||
- Merge ImapConfig into Imap.
|
||||
- Get rid of ImapActionResult.
|
||||
- Build contexts using ContextBuilder.
|
||||
- Do not send `Secure-Join-Group` in `vg-request`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Fix `test_verified_oneonone_chat_broken_by_device_change()` ([#5280](https://github.com/deltachat/deltachat-core-rust/pull/5280)).
|
||||
- `get_protected_chat()`: Use FFIEventTracker instead of `dc_wait_next_msgs()` ([#5207](https://github.com/deltachat/deltachat-core-rust/pull/5207)).
|
||||
- Fixup `tests/test_3_offline.py::TestOfflineAccountBasic::test_wrong_db`.
|
||||
- Fix pytest compat ([#5317](https://github.com/deltachat/deltachat-core-rust/pull/5317)).
|
||||
|
||||
## [1.135.1] - 2024-02-20
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Sync self-avatar across devices ([#4893](https://github.com/deltachat/deltachat-core-rust/pull/4893)).
|
||||
- Sync Config::Selfstatus across devices ([#4893](https://github.com/deltachat/deltachat-core-rust/pull/4893)).
|
||||
- Remove webxdc sending limit.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Never encrypt `{vc,vg}-request` SecureJoin messages.
|
||||
- Apply Autocrypt headers if timestamp is unchanged.
|
||||
- `Context::get_info`: Report displayname as "displayname" (w/o underscore).
|
||||
|
||||
### Tests
|
||||
|
||||
- Mock `SystemTime::now()` for the tests.
|
||||
- Add a test on protection message sort timestamp ([#5088](https://github.com/deltachat/deltachat-core-rust/pull/5088)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Add flake.nix.
|
||||
- Add footer template for git-cliff.
|
||||
|
||||
### CI
|
||||
|
||||
- Update GitHub Actions `actions/upload-artifact`, `actions/download-artifact`, `actions/checkout`.
|
||||
- Build deltachat-repl for Windows with nix.
|
||||
- Build deltachat-rpc-server with nix.
|
||||
- Try to upload deltachat-rpc-server only on release.
|
||||
- Fixup node-package.yml after artifact actions upgrade.
|
||||
- Update to actions/checkout@v4.
|
||||
- Replace download-artifact v1 with v4.
|
||||
|
||||
### Refactor
|
||||
|
||||
- `create_keypair`: Remove unnecessary `map_err`.
|
||||
- Return error with a cause when failing to export keys.
|
||||
- Rename incorrectly named variables in `create_keypair`.
|
||||
|
||||
## [1.135.0] - 2024-02-13
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add wildcard pattern support to provider database.
|
||||
- Add device message about outgoing undecryptable messages ([#5164](https://github.com/deltachat/deltachat-core-rust/pull/5164)).
|
||||
- Context::set_config(): Restart IO scheduler if needed ([#5111](https://github.com/deltachat/deltachat-core-rust/pull/5111)).
|
||||
- Server_sent_unsolicited_exists(): Log folder name.
|
||||
- Cache system time instead of looking at the clock several times in a row.
|
||||
- Basic self-reporting ([#5129](https://github.com/deltachat/deltachat-core-rust/pull/5129)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Dehtml: Don't just truncate text when trying to decode ([#5223](https://github.com/deltachat/deltachat-core-rust/pull/5223)).
|
||||
- Mark the gossip keys from the message as verified, not the ones from the db ([#5247](https://github.com/deltachat/deltachat-core-rust/pull/5247)).
|
||||
- Guarantee immediate message deletion if delete_server_after == 0 ([#5201](https://github.com/deltachat/deltachat-core-rust/pull/5201)).
|
||||
- Never allow a message timestamp to be a lot in the future ([#5249](https://github.com/deltachat/deltachat-core-rust/pull/5249)).
|
||||
- Imap::configure_mvbox: Do select_with_uidvalidity() before return.
|
||||
- ImapSession::select_or_create_folder(): Don't fail if folder is created in parallel.
|
||||
- Emit ConfigSynced event on the second device.
|
||||
- Create mvbox on setting mvbox_move.
|
||||
- Use SystemTime instead of Instant everywhere.
|
||||
- Restore database rows removed in previous release; this ensures compatibility when adding second device or importing backup and not all devices run the new core ([#5254](https://github.com/deltachat/deltachat-core-rust/pull/5254))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump image from 0.24.7 to 0.24.8.
|
||||
- cargo: Bump chrono from 0.4.31 to 0.4.33.
|
||||
- cargo: Bump futures-lite from 2.1.0 to 2.2.0.
|
||||
- cargo: Bump pin-project from 1.1.3 to 1.1.4.
|
||||
- cargo: Bump iana-time-zone from yanked 0.1.59 to 0.1.60.
|
||||
- cargo: Bump smallvec from 1.11.2 to 1.13.1.
|
||||
- cargo: Bump base64 from 0.21.5 to 0.21.7.
|
||||
- cargo: Bump regex from 1.10.2 to 1.10.3.
|
||||
- cargo: Bump libc from 0.2.151 to 0.2.153.
|
||||
- cargo: Bump reqwest from 0.11.23 to 0.11.24.
|
||||
- cargo: Bump axum from 0.7.3 to 0.7.4.
|
||||
- cargo: Bump uuid from 1.6.1 to 1.7.0.
|
||||
- cargo: Bump fast-socks5 from 0.9.2 to 0.9.5.
|
||||
- cargo: Bump serde_json from 1.0.111 to 1.0.113.
|
||||
- cargo: Bump syn from 2.0.46 to 2.0.48.
|
||||
- cargo: Bump serde from 1.0.194 to 1.0.196.
|
||||
- cargo: Bump toml from 0.8.8 to 0.8.10.
|
||||
- cargo: Update to strum 0.26.
|
||||
- Cargo update.
|
||||
- scripts: Do not install deltachat-rpc-client twice.
|
||||
|
||||
### Other
|
||||
|
||||
- Update welcome image, thanks @paulaluap
|
||||
- Merge pull request #5243 from deltachat/dependabot/cargo/pin-project-1.1.4
|
||||
- Merge pull request #5241 from deltachat/dependabot/cargo/futures-lite-2.2.0
|
||||
- Merge pull request #5236 from deltachat/dependabot/cargo/chrono-0.4.33
|
||||
- Merge pull request #5235 from deltachat/dependabot/cargo/image-0.24.8
|
||||
|
||||
|
||||
### Refactor
|
||||
|
||||
- Resultify token::exists.
|
||||
|
||||
### Tests
|
||||
|
||||
- Delete_server_after="1" should cause immediate message deletion ([#5201](https://github.com/deltachat/deltachat-core-rust/pull/5201)).
|
||||
|
||||
## [1.134.0] - 2024-01-31
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] JSON-RPC: device message api now requires `Option<MessageData>` instead of `String` for the message ([#5211](https://github.com/deltachat/deltachat-core-rust/pull/5211)).
|
||||
- CFFI: add `dc_accounts_background_fetch` and event `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE`.
|
||||
- JSON-RPC: add `accounts_background_fetch`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- `Qr::check_qr()`: Accept i.delta.chat invite links ([#5217](https://github.com/deltachat/deltachat-core-rust/pull/5217)).
|
||||
- Add support for IMAP METADATA, fetching `/shared/comment` and `/shared/admin` and displaying it in account info.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Add tolerance for macOS and iOS changing `#` to `%23`.
|
||||
- Do not drop unknown report attachments, such as TLS reports.
|
||||
- Treat only "Auto-Submitted: auto-generated" messages as bot-sent ([#5213](https://github.com/deltachat/deltachat-core-rust/pull/5213)).
|
||||
- `Chat::resend_msgs`: Guarantee strictly increasing time in the `Date` header.
|
||||
- Delete resent messages on receiver side ([#5155](https://github.com/deltachat/deltachat-core-rust/pull/5155)).
|
||||
- Fix iOS build issue.
|
||||
|
||||
### CI
|
||||
|
||||
- Add/remove necessary newlines to fix Python lint.
|
||||
|
||||
### Tests
|
||||
|
||||
- `test_import_export_online_all`: Send the message to the existing address to avoid errors ([#5220](https://github.com/deltachat/deltachat-core-rust/pull/5220)).
|
||||
|
||||
## [1.133.2] - 2024-01-24
|
||||
|
||||
### Fixes
|
||||
|
||||
- Downgrade OpenSSL from 3.2.0 to 3.1.4 ([#5206](https://github.com/deltachat/deltachat-core-rust/issues/5206))
|
||||
- No new chats for MDNs with alias ([#5196](https://github.com/deltachat/deltachat-core-rust/issues/5196)) ([#5199](https://github.com/deltachat/deltachat-core-rust/pull/5199)).
|
||||
|
||||
## [1.133.1] - 2024-01-21
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add `is_bot` to cffi and jsonrpc ([#5197](https://github.com/deltachat/deltachat-core-rust/pull/5197)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add system message when provider does not allow unencrypted messages ([#5195](https://github.com/deltachat/deltachat-core-rust/pull/5195)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- `Chat::send_msg`: Remove encryption-related params from already sent message. This allows to send received encrypted `dc_msg_t` object to unencrypted chat, e.g. in a Python bot.
|
||||
- Set message download state to Failure on IMAP errors. This avoids partially downloaded messages getting stuck in "Downloading..." state without actually being in a download queue.
|
||||
- BCC-to-self even if server deletion is set to "at once". This is a workaround for SMTP servers which do not return response in time, BCC-self works as a confirmation that message was sent out successfully and does not need more retries.
|
||||
- node: Run tests with native ESM modules instead of `esm` ([#5194](https://github.com/deltachat/deltachat-core-rust/pull/5194)).
|
||||
- Use Quoted-Printable MIME encoding for the text part ([#3986](https://github.com/deltachat/deltachat-core-rust/pull/3986)).
|
||||
|
||||
### Tests
|
||||
|
||||
- python: Add `get_protected_chat` to testplugin.py.
|
||||
|
||||
## [1.133.0] - 2024-01-14
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Securejoin protocol implementation refinements
|
||||
- Track forward and backward verification separately ([#5089](https://github.com/deltachat/deltachat-core-rust/pull/5089)) to avoid inconsistent states.
|
||||
- Mark 1:1 chat as verified for Bob early. 1:1 chat with Alice is verified as soon as Alice's key is verified rather than at the end of the protocol.
|
||||
- Put Message-ID into hidden headers and take it from there on receiver ([#4798](https://github.com/deltachat/deltachat-core-rust/pull/4798)). This works around servers which generate their own Message-ID and overwrite the one generated by Delta Chat.
|
||||
- deltachat-repl: Enable INFO logging by default and add timestamps.
|
||||
- Add `ConfigSynced` (`DC_EVENT_CONFIG_SYNCED`) event which is emitted when configuration is changed via synchronization message or synchronization message for configuration is sent. UI may refresh elments based on the configuration key which is a part of the event.
|
||||
- Sync contact creation/rename across devices ([#5163](https://github.com/deltachat/deltachat-core-rust/pull/5163)).
|
||||
- Encrypt MDNs ([#5175](https://github.com/deltachat/deltachat-core-rust/pull/5175)).
|
||||
- Only try to configure non-strict TLS checks if explicitly set ([#5181](https://github.com/deltachat/deltachat-core-rust/pull/5181)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Use released version of iroh 0.4.2 for "setup second device" feature.
|
||||
|
||||
### CI
|
||||
|
||||
- Update to Rust 1.75.0.
|
||||
- Downgrade `chai` from 4.4.0 to 4.3.10.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add a link <https://www.ietf.org/archive/id/draft-bucksch-autoconfig-00.html> to autoconfig RFC draft.
|
||||
- Update securejoin link in `standards.md` from <https://countermitm.readthedocs.io/> to <https://securejoin.readthedocs.io>.
|
||||
- Restore "Constants" page in Doxygen >=1.9.8
|
||||
|
||||
### Fixes
|
||||
|
||||
- imap: Limit the rate of LOGIN attempts rather than connection attempts. This is to avoid having to wait for rate limiter right after switching from a bad or offline network to a working network while still guarding against reconnection loop.
|
||||
- Do not ignore `peerstate.save_to_db()` errors.
|
||||
- securejoin: Mark 1:1s as protected regardless of the Config::VerifiedOneOnOneChats.
|
||||
- Delete received outgoing messages from SMTP queue ([#5115](https://github.com/deltachat/deltachat-core-rust/pull/5115)).
|
||||
- imap: Fail fast on `LIST` errors to avoid busy loop when connection is lost.
|
||||
- Split SMTP jobs already in `chat::create_send_msg_jobs()` ([#5115](https://github.com/deltachat/deltachat-core-rust/pull/5115)).
|
||||
- Do not remove contents from unencrypted [Schleuder](https://schleuder.org/) mailing lists messages.
|
||||
- Reset message error when scheduling resending ([#5119](https://github.com/deltachat/deltachat-core-rust/pull/5119)).
|
||||
- Emit events more reliably when starting and stopping I/O ([#5101](https://github.com/deltachat/deltachat-core-rust/pull/5101)).
|
||||
- Fix timestamp of chat protection info message for correct message ordering after restoring a backup ([#5088](https://github.com/deltachat/deltachat-core-rust/pull/5088)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- sql: Recreate `config` table with UNIQUE constraint.
|
||||
- sql: Recreate `keypairs` table to remove unused `addr` and `created` fields and move `is_default` flag to `config` table.
|
||||
- Send `Secure-Join-Fingerprint` only in `*-request-with-auth`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Test joining non-protected group.
|
||||
- Test that read receipts don't degrade encryption.
|
||||
- Test that changing default private key breaks backward verification.
|
||||
- Test recovery from lost vc-contact-confirm.
|
||||
- Use `wait_for_incoming_msg_event()` more.
|
||||
|
||||
## [1.132.1] - 2023-12-12
|
||||
|
||||
### Features / Changes
|
||||
@@ -3357,3 +3965,21 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.131.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.8...v1.131.9
|
||||
[1.132.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.9...v1.132.0
|
||||
[1.132.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.132.0...v1.132.1
|
||||
[1.133.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.132.1...v1.133.0
|
||||
[1.133.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.0...v1.133.1
|
||||
[1.133.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.1...v1.133.2
|
||||
[1.134.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.2...v1.134.0
|
||||
[1.135.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.134.0...v1.135.0
|
||||
[1.135.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.135.0...v1.135.1
|
||||
[1.136.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.135.1...v1.136.0
|
||||
[1.136.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.0...v1.136.1
|
||||
[1.136.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.1...v1.136.2
|
||||
[1.136.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.2...v1.136.3
|
||||
[1.136.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.3...v1.136.4
|
||||
[1.136.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.4...v1.136.5
|
||||
[1.136.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.5...v1.136.6
|
||||
[1.137.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.6...v1.137.0
|
||||
[1.137.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.0...v1.137.1
|
||||
[1.137.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.1...v1.137.2
|
||||
[1.137.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.2...v1.137.3
|
||||
[1.137.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.3...v1.137.4
|
||||
|
||||
@@ -12,26 +12,22 @@ else()
|
||||
set(DYNAMIC_EXT "dll")
|
||||
endif()
|
||||
|
||||
if(DEFINED ENV{CARGO_BUILD_TARGET})
|
||||
set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
|
||||
else()
|
||||
set(ARCH_DIR "./")
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
"target/release/libdeltachat.a"
|
||||
"target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"target/release/pkgconfig/deltachat.pc"
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||
COMMAND
|
||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
||||
${CARGO} build --release --no-default-features --features jsonrpc
|
||||
|
||||
# Build in `deltachat-ffi` directory instead of using
|
||||
# `--package deltachat_ffi` to avoid feature resolver version
|
||||
# "1" bug which makes `--no-default-features` affect only
|
||||
# `deltachat`, but not `deltachat-ffi` package.
|
||||
#
|
||||
# We can't enable version "2" resolver [1] because it is not
|
||||
# stable yet on rust 1.50.0.
|
||||
#
|
||||
# [1] https://doc.rust-lang.org/nightly/cargo/reference/features.html#resolver-version-2-command-line-flags
|
||||
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||
)
|
||||
|
||||
@@ -39,12 +35,12 @@ add_custom_target(
|
||||
lib_deltachat
|
||||
ALL
|
||||
DEPENDS
|
||||
"target/release/libdeltachat.a"
|
||||
"target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"target/release/pkgconfig/deltachat.pc"
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||
)
|
||||
|
||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
|
||||
1215
Cargo.lock
generated
1215
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
71
Cargo.toml
71
Cargo.toml
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.132.1"
|
||||
version = "1.137.4"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.70"
|
||||
rust-version = "1.77"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
@@ -30,37 +31,37 @@ opt-level = "z"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
[patch.crates-io]
|
||||
imap-proto = { git = "https://github.com/djc/tokio-imap.git", rev = "01ff256a7e42a9f7d2732706f8b71a16ce93427e" }
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
deltachat-time = { path = "./deltachat-time" }
|
||||
deltachat-contact-tools = { path = "./deltachat-contact-tools" }
|
||||
format-flowed = { path = "./format-flowed" }
|
||||
ratelimit = { path = "./deltachat-ratelimit" }
|
||||
|
||||
anyhow = "1"
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.0"
|
||||
async-channel = "2.0.0"
|
||||
async-imap = { version = "0.9.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-imap = { version = "0.9.7", default-features = false, features = ["runtime-tokio"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||
backtrace = "0.3"
|
||||
base64 = "0.21"
|
||||
brotli = { version = "3.4", default-features=false, features = ["std"] }
|
||||
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
|
||||
base64 = "0.22"
|
||||
brotli = { version = "5", default-features=false, features = ["std"] }
|
||||
chrono = { version = "0.4.37", default-features=false, features = ["clock", "std"] }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||
escaper = "0.1"
|
||||
fast-socks5 = "0.9"
|
||||
fd-lock = "4"
|
||||
futures = "0.3"
|
||||
futures-lite = "2.0.0"
|
||||
futures-lite = "2.3.0"
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "0.24"
|
||||
humansize = "2"
|
||||
image = { version = "0.24.7", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh = { version = "0.4.2", default-features = false }
|
||||
kamadak-exif = "0.5"
|
||||
kamadak-exif = "0.5.3"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
mailparse = "0.14"
|
||||
@@ -68,50 +69,58 @@ mime = "0.3.17"
|
||||
num_cpus = "1.16"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.18.0"
|
||||
once_cell = { workspace = true }
|
||||
percent-encoding = "2.3"
|
||||
parking_lot = "0.12"
|
||||
pgp = { version = "0.10", default-features = false }
|
||||
pin-project = "1"
|
||||
pgp = { version = "0.11", default-features = false }
|
||||
pretty_env_logger = { version = "0.5", optional = true }
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.31"
|
||||
quoted_printable = "0.5"
|
||||
rand = "0.8"
|
||||
regex = "1.9"
|
||||
reqwest = { version = "0.11.23", features = ["json"] }
|
||||
rusqlite = { version = "0.30", features = ["sqlcipher"] }
|
||||
regex = { workspace = true }
|
||||
reqwest = { version = "0.12.2", features = ["json"] }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
sanitize-filename = "0.5"
|
||||
serde_json = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
smallvec = "1"
|
||||
strum = "0.25"
|
||||
strum_macros = "0.25"
|
||||
smallvec = "1.13.2"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.0"
|
||||
textwrap = "0.16.1"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-stream = { version = "0.1.14", features = ["fs"] }
|
||||
tokio-stream = { version = "0.1.15", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = "0.7.9"
|
||||
toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
# Pin OpenSSL to 3.1 releases.
|
||||
# OpenSSL 3.2 has a regression tracked at <https://github.com/openssl/openssl/issues/23376>
|
||||
# which results in broken `deltachat-rpc-server` binaries when cross-compiled using Zig toolchain.
|
||||
# See <https://github.com/deltachat/deltachat-core-rust/issues/5206> for Delta Chat issue.
|
||||
# According to <https://www.openssl.org/policies/releasestrat.html>
|
||||
# 3.1 branch will be supported until 2025-03-14.
|
||||
openssl-src = "~300.1"
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
futures-lite = "2.0.0"
|
||||
futures-lite = "2.3.0"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
testdir = "0.9.0"
|
||||
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.37.0", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
|
||||
[workspace]
|
||||
@@ -122,7 +131,9 @@ members = [
|
||||
"deltachat-rpc-server",
|
||||
"deltachat-ratelimit",
|
||||
"deltachat-repl",
|
||||
"deltachat-time",
|
||||
"format-flowed",
|
||||
"deltachat-contact-tools",
|
||||
]
|
||||
|
||||
[[bench]]
|
||||
@@ -153,6 +164,12 @@ harness = false
|
||||
name = "send_events"
|
||||
harness = false
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
once_cell = "1.18.0"
|
||||
regex = "1.10"
|
||||
rusqlite = { version = "0.31" }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
|
||||
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
|
||||
</a>
|
||||
<a href="https://deps.rs/repo/github/deltachat/deltachat-core-rust">
|
||||
<img alt="dependency status" src="https://deps.rs/repo/github/deltachat/deltachat-core-rust/status.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -192,6 +195,7 @@ or its language bindings:
|
||||
- [Desktop](https://github.com/deltachat/deltachat-desktop)
|
||||
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
|
||||
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
||||
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
|
||||
- several **Bots**
|
||||
|
||||
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.
|
||||
|
||||
13
RELEASE.md
13
RELEASE.md
@@ -9,13 +9,16 @@ For example, to release version 1.116.0 of the core, do the following steps.
|
||||
|
||||
3. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
|
||||
|
||||
4. Update the version by running `scripts/set_core_version.py 1.116.0`.
|
||||
4. add a link to compare previous with current version to the end of CHANGELOG.md:
|
||||
`[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.2...v1.116.0`
|
||||
|
||||
5. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||
5. Update the version by running `scripts/set_core_version.py 1.116.0`.
|
||||
|
||||
6. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
||||
|
||||
6. Tag the release: `git tag -a v1.116.0`.
|
||||
7. Tag the release: `git tag -a v1.116.0`.
|
||||
|
||||
7. Push the release tag: `git push origin v1.116.0`.
|
||||
8. Push the release tag: `git push origin v1.116.0`.
|
||||
|
||||
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.
|
||||
9. Create a GitHub release: `gh release create v1.116.0 -n ''`.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 121 KiB |
14
cliff.toml
14
cliff.toml
@@ -77,3 +77,17 @@ body = """
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
footer = """
|
||||
{% for release in releases -%}
|
||||
{% if release.version -%}
|
||||
{% if release.previous.version -%}
|
||||
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
||||
https://github.com/deltachat/deltachat-core-rust\
|
||||
/compare/{{ release.previous.version }}..{{ release.version }}
|
||||
{% endif -%}
|
||||
{% else -%}
|
||||
[unreleased]: https://github.com/deltachat/deltachat-core-rust\
|
||||
/compare/{{ release.previous.version }}..HEAD
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
18
deltachat-contact-tools/Cargo.toml
Normal file
18
deltachat-contact-tools/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "deltachat-contact-tools"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Contact-related tools, like parsing vcards and sanitizing name and address"
|
||||
license = "MPL-2.0"
|
||||
# TODO maybe it should be called "deltachat-text-utils" or similar?
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
280
deltachat-contact-tools/src/lib.rs
Normal file
280
deltachat-contact-tools/src/lib.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
//! Contact-related tools, like parsing vcards and sanitizing name and address
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(
|
||||
unused,
|
||||
clippy::correctness,
|
||||
missing_debug_implementations,
|
||||
missing_docs,
|
||||
clippy::all,
|
||||
clippy::wildcard_imports,
|
||||
clippy::needless_borrow,
|
||||
clippy::cast_lossless,
|
||||
clippy::unused_async,
|
||||
clippy::explicit_iter_loop,
|
||||
clippy::explicit_into_iter_loop,
|
||||
clippy::cloned_instead_of_copied
|
||||
)]
|
||||
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
clippy::mixed_read_write_in_expression,
|
||||
clippy::bool_assert_comparison,
|
||||
clippy::manual_split_once,
|
||||
clippy::format_push_string,
|
||||
clippy::bool_to_int_with_if
|
||||
)]
|
||||
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
|
||||
use anyhow::bail;
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
/// Valid contact address.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContactAddress(String);
|
||||
|
||||
impl Deref for ContactAddress {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ContactAddress {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContactAddress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ContactAddress {
|
||||
/// Constructs a new contact address from string,
|
||||
/// normalizing and validating it.
|
||||
pub fn new(s: &str) -> Result<Self> {
|
||||
let addr = addr_normalize(s);
|
||||
if !may_be_valid_addr(&addr) {
|
||||
bail!("invalid address {:?}", s);
|
||||
}
|
||||
Ok(Self(addr.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow converting [`ContactAddress`] to an SQLite type.
|
||||
impl rusqlite::types::ToSql for ContactAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Text(self.0.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the name and address
|
||||
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.is_empty() {
|
||||
strip_rtlo_characters(
|
||||
&captures
|
||||
.get(1)
|
||||
.map_or("".to_string(), |m| normalize_name(m.as_str())),
|
||||
)
|
||||
} else {
|
||||
strip_rtlo_characters(name)
|
||||
},
|
||||
captures
|
||||
.get(2)
|
||||
.map_or("".to_string(), |m| m.as_str().to_string()),
|
||||
)
|
||||
} else {
|
||||
(strip_rtlo_characters(name), addr.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize a name.
|
||||
///
|
||||
/// - Remove quotes (come from some bad MUA implementations)
|
||||
/// - Trims the resulting string
|
||||
///
|
||||
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
|
||||
pub fn normalize_name(full_name: &str) -> String {
|
||||
let full_name = full_name.trim();
|
||||
if full_name.is_empty() {
|
||||
return full_name.into();
|
||||
}
|
||||
|
||||
match full_name.as_bytes() {
|
||||
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
|
||||
.get(1..full_name.len() - 1)
|
||||
.map_or("".to_string(), |s| s.trim().to_string()),
|
||||
_ => full_name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}'];
|
||||
/// This method strips all occurrences of the RTLO Unicode character.
|
||||
/// [Why is this needed](https://github.com/deltachat/deltachat-core-rust/issues/3479)?
|
||||
pub fn strip_rtlo_characters(input_str: &str) -> String {
|
||||
input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "")
|
||||
}
|
||||
|
||||
/// Returns false if addr is an invalid address, otherwise true.
|
||||
pub fn may_be_valid_addr(addr: &str) -> bool {
|
||||
let res = EmailAddress::new(addr);
|
||||
res.is_ok()
|
||||
}
|
||||
|
||||
/// Returns address lowercased,
|
||||
/// with whitespace trimmed and `mailto:` prefix removed.
|
||||
pub fn addr_normalize(addr: &str) -> String {
|
||||
let norm = addr.trim().to_lowercase();
|
||||
|
||||
if norm.starts_with("mailto:") {
|
||||
norm.get(7..).unwrap_or(&norm).to_string()
|
||||
} else {
|
||||
norm
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares two email addresses, normalizing them beforehand.
|
||||
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
|
||||
let norm1 = addr_normalize(addr1);
|
||||
let norm2 = addr_normalize(addr2);
|
||||
|
||||
norm1 == norm2
|
||||
}
|
||||
|
||||
///
|
||||
/// Represents an email address, right now just the `name@domain` portion.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use deltachat_contact_tools::EmailAddress;
|
||||
/// let email = match EmailAddress::new("someone@example.com") {
|
||||
/// Ok(addr) => addr,
|
||||
/// Err(e) => panic!("Error parsing address, error was {}", e),
|
||||
/// };
|
||||
/// assert_eq!(&email.local, "someone");
|
||||
/// assert_eq!(&email.domain, "example.com");
|
||||
/// assert_eq!(email.to_string(), "someone@example.com");
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct EmailAddress {
|
||||
/// Local part of the email address.
|
||||
pub local: String,
|
||||
|
||||
/// Email address domain.
|
||||
pub domain: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for EmailAddress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}@{}", self.local, self.domain)
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailAddress {
|
||||
/// Performs a dead-simple parse of an email address.
|
||||
pub fn new(input: &str) -> Result<EmailAddress> {
|
||||
if input.is_empty() {
|
||||
bail!("empty string is not valid");
|
||||
}
|
||||
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
|
||||
|
||||
if input
|
||||
.chars()
|
||||
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
||||
{
|
||||
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
|
||||
}
|
||||
|
||||
match &parts[..] {
|
||||
[domain, local] => {
|
||||
if local.is_empty() {
|
||||
bail!("empty string is not valid for local part in {:?}", input);
|
||||
}
|
||||
if domain.is_empty() {
|
||||
bail!("missing domain after '@' in {:?}", input);
|
||||
}
|
||||
if domain.ends_with('.') {
|
||||
bail!("Domain {domain:?} should not contain the dot in the end");
|
||||
}
|
||||
Ok(EmailAddress {
|
||||
local: (*local).to_string(),
|
||||
domain: (*domain).to_string(),
|
||||
})
|
||||
}
|
||||
_ => bail!("Email {:?} must contain '@' character", input),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for EmailAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Text(self.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_contact_address() -> Result<()> {
|
||||
let alice_addr = "alice@example.org";
|
||||
let contact_address = ContactAddress::new(alice_addr)?;
|
||||
assert_eq!(contact_address.as_ref(), alice_addr);
|
||||
|
||||
let invalid_addr = "<> foobar";
|
||||
assert!(ContactAddress::new(invalid_addr).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_emailaddress_parse() {
|
||||
assert_eq!(EmailAddress::new("").is_ok(), false);
|
||||
assert_eq!(
|
||||
EmailAddress::new("user@domain.tld").unwrap(),
|
||||
EmailAddress {
|
||||
local: "user".into(),
|
||||
domain: "domain.tld".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
EmailAddress::new("user@localhost").unwrap(),
|
||||
EmailAddress {
|
||||
local: "user".into(),
|
||||
domain: "localhost".into()
|
||||
}
|
||||
);
|
||||
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
|
||||
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
|
||||
assert!(EmailAddress::new("tt.dd@uu").is_ok());
|
||||
assert!(EmailAddress::new("u@d").is_ok());
|
||||
assert!(EmailAddress::new("u@d.").is_err());
|
||||
assert!(EmailAddress::new("u@d.t").is_ok());
|
||||
assert_eq!(
|
||||
EmailAddress::new("u@d.tt").unwrap(),
|
||||
EmailAddress {
|
||||
local: "u".into(),
|
||||
domain: "d.tt".into(),
|
||||
}
|
||||
);
|
||||
assert!(EmailAddress::new("u@tt").is_ok());
|
||||
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.132.1"
|
||||
version = "1.137.4"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
@@ -20,7 +20,7 @@ libc = "0.2"
|
||||
human-panic = { version = "1", default-features = false }
|
||||
num-traits = "0.2"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
tokio = { version = "1.37.0", features = ["rt-multi-thread"] }
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
rand = "0.8"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<tab type="hierarchy" visible="no" title="" intro=""/>
|
||||
<tab type="classmembers" visible="no" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="modules" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
||||
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
||||
<tab type="pages" visible="yes" title="" intro=""/>
|
||||
<tab type="namespaces" visible="yes" title="">
|
||||
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
||||
|
||||
@@ -17,7 +17,6 @@ typedef struct _dc_array dc_array_t;
|
||||
typedef struct _dc_chatlist dc_chatlist_t;
|
||||
typedef struct _dc_chat dc_chat_t;
|
||||
typedef struct _dc_msg dc_msg_t;
|
||||
typedef struct _dc_reactions dc_reactions_t;
|
||||
typedef struct _dc_contact dc_contact_t;
|
||||
typedef struct _dc_lot dc_lot_t;
|
||||
typedef struct _dc_provider dc_provider_t;
|
||||
@@ -363,8 +362,12 @@ uint32_t dc_get_id (dc_context_t* context);
|
||||
* Must be freed using dc_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per context.
|
||||
* Having more than one event emitter running at the same time on the same context
|
||||
* will result in events being randomly delivered to one of the emitters.
|
||||
* The result of having multiple event emitters is unspecified.
|
||||
* Currently events are broadcasted to all existing event emitters,
|
||||
* but previous versions delivered events to only one event emitter
|
||||
* and this behavior may change again in the future.
|
||||
* Events emitted before creation of event emitter
|
||||
* may or may not be available to event emitter.
|
||||
*/
|
||||
dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
|
||||
|
||||
@@ -423,19 +426,16 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* Sending messages to self is needed for a proper multi-account setup,
|
||||
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
|
||||
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
|
||||
* 0=do not watch the `Sent`-folder (default),
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* 0=do not watch the `Sent`-folder (default).
|
||||
* - `mvbox_move` = 1=detect chat messages,
|
||||
* move them to the `DeltaChat` folder,
|
||||
* and watch the `DeltaChat` folder for updates (default),
|
||||
* 0=do not move chat-messages
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `only_fetch_mvbox` = 1=Do not fetch messages from folders other than the
|
||||
* `DeltaChat` folder. Messages will still be fetched from the
|
||||
* spam folder and `sendbox_watch` will also still be respected
|
||||
* if enabled.
|
||||
* 0=watch all folders normally (default)
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
|
||||
* show direct replies to chats only,
|
||||
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
|
||||
@@ -689,8 +689,25 @@ int dc_get_connectivity (dc_context_t* context);
|
||||
char* dc_get_connectivity_html (dc_context_t* context);
|
||||
|
||||
|
||||
#define DC_PUSH_NOT_CONNECTED 0
|
||||
#define DC_PUSH_HEARTBEAT 1
|
||||
#define DC_PUSH_CONNECTED 2
|
||||
|
||||
/**
|
||||
* Get the current push notification state.
|
||||
* One of:
|
||||
* - DC_PUSH_NOT_CONNECTED
|
||||
* - DC_PUSH_HEARTBEAT
|
||||
* - DC_PUSH_CONNECTED
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @return Push notification state.
|
||||
*/
|
||||
int dc_get_push_state (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Standalone version of dc_accounts_all_work_done().
|
||||
* Only used by the python tests.
|
||||
*/
|
||||
int dc_all_work_done (dc_context_t* context);
|
||||
@@ -1103,36 +1120,6 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
|
||||
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Send a reaction to message.
|
||||
*
|
||||
* Reaction is a string of emojis separated by spaces. Reaction to a
|
||||
* single message can be sent multiple times. The last reaction
|
||||
* received overrides all previously received reactions. It is
|
||||
* possible to remove all reactions by sending an empty string.
|
||||
*
|
||||
* @deprecated 2023-11-27, use jsonrpc method `send_reaction` instead
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id ID of the message you react to.
|
||||
* @param reaction A string consisting of emojis separated by spaces.
|
||||
* @return The ID of the message sent out or 0 for errors.
|
||||
*/
|
||||
uint32_t dc_send_reaction (dc_context_t* context, uint32_t msg_id, char *reaction);
|
||||
|
||||
|
||||
/**
|
||||
* Get a structure with reactions to the message.
|
||||
*
|
||||
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The message ID to get reactions for.
|
||||
* @return A structure with all reactions to the message.
|
||||
*/
|
||||
dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* A webxdc instance sends a status update to its other members.
|
||||
*
|
||||
@@ -1195,6 +1182,65 @@ int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const
|
||||
*/
|
||||
char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t serial);
|
||||
|
||||
|
||||
/**
|
||||
* Set Webxdc file as integration.
|
||||
* see dc_init_webxdc_integration() for more details about Webxdc integrations.
|
||||
*
|
||||
* @warning This is an experimental API which may change in the future
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param file The .xdc file to use as Webxdc integration.
|
||||
*/
|
||||
void dc_set_webxdc_integration (dc_context_t* context, const char* file);
|
||||
|
||||
|
||||
/**
|
||||
* Init a Webxdc integration.
|
||||
*
|
||||
* A Webxdc integration is
|
||||
* a Webxdc showing a map, getting locations via setUpdateListener(), setting POIs via sendUpdate();
|
||||
* core takes eg. care of feeding locations to the Webxdc or sending the data out.
|
||||
*
|
||||
* @warning This is an experimental API, esp. support of integration types (eg. image editor, tools) is left out for simplicity
|
||||
*
|
||||
* Currently, Webxdc integrations are .xdc files shipped together with the main app.
|
||||
* Before dc_init_webxdc_integration() can be called,
|
||||
* UI has to call dc_set_webxdc_integration() to define a .xdc file to be used as integration.
|
||||
*
|
||||
* dc_init_webxdc_integration() returns a Webxdc message ID that
|
||||
* UI can open and use mostly as usual.
|
||||
*
|
||||
* Concrete behaviour and status updates depend on the integration, driven by UI needs.
|
||||
*
|
||||
* There is no need to de-initialize the integration,
|
||||
* however, unless documented otherwise,
|
||||
* the integration is valid only as long as not re-initialized
|
||||
* In other words, UI must not have a Webxdc with the same integration open twice.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ~~~
|
||||
* // Define a .xdc file to be used as maps integration
|
||||
* dc_set_webxdc_integration(context, path_to_maps_xdc);
|
||||
*
|
||||
* // Integrate the map to a chat, the map will show locations for this chat then:
|
||||
* uint32_t webxdc_instance = dc_init_webxdc_integration(context, any_chat_id);
|
||||
*
|
||||
* // Or use the Webxdc as a global map, showing locations of all chats:
|
||||
* uint32_t webxdc_instance = dc_init_webxdc_integration(context, 0);
|
||||
* ~~~
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat to get the integration for.
|
||||
* @return ID of the message that refers to the Webxdc instance.
|
||||
* UI can open a Webxdc as usual with this instance.
|
||||
*/
|
||||
uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Save a draft for a chat in the database.
|
||||
*
|
||||
@@ -2562,7 +2608,7 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
|
||||
* the Verified-Group-Invite protocol is offered in the QR code;
|
||||
* works for protected groups as well as for normal groups.
|
||||
* If set to 0, the Setup-Contact protocol is offered in the QR code.
|
||||
* See https://securejoin.readthedocs.io/en/latest/new.html
|
||||
* See https://securejoin.delta.chat/
|
||||
* for details about both protocols.
|
||||
* @return The text that should go to the QR code,
|
||||
* On errors, an empty QR code is returned, NULL is never returned.
|
||||
@@ -2598,8 +2644,7 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
|
||||
*
|
||||
* Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes.
|
||||
*
|
||||
* See https://securejoin.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
* See https://securejoin.delta.chat/ for details about both protocols.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
@@ -3084,23 +3129,6 @@ dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);
|
||||
int dc_accounts_select_account (dc_accounts_t* accounts, uint32_t account_id);
|
||||
|
||||
|
||||
/**
|
||||
* This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
|
||||
*
|
||||
* iOS can:
|
||||
* - call dc_start_io() (in case IO was not running)
|
||||
* - call dc_maybe_network()
|
||||
* - while dc_accounts_all_work_done() returns false:
|
||||
* - Wait for #DC_EVENT_CONNECTIVITY_CHANGED
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
* @return Whether all accounts finished their background work.
|
||||
* #DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
|
||||
*/
|
||||
int dc_accounts_all_work_done (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
* Start job and IMAP/SMTP tasks for all accounts managed by the account manager.
|
||||
* If IO is already running, nothing happens.
|
||||
@@ -3151,6 +3179,33 @@ void dc_accounts_maybe_network (dc_accounts_t* accounts);
|
||||
void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
* Perform a background fetch for all accounts in parallel with a timeout.
|
||||
* Pauses the scheduler, fetches messages from imap and then resumes the scheduler.
|
||||
*
|
||||
* dc_accounts_background_fetch() was created for the iOS Background fetch.
|
||||
*
|
||||
* The `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE` event is emitted at the end
|
||||
* even in case of timeout, unless the function fails and returns 0.
|
||||
* Process all events until you get this one and you can safely return to the background
|
||||
* without forgetting to create notifications caused by timing race conditions.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param timeout The timeout in seconds
|
||||
* @return Return 1 if DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE was emitted and 0 otherwise.
|
||||
*/
|
||||
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
|
||||
|
||||
|
||||
/**
|
||||
* Sets device token for Apple Push Notification service.
|
||||
* Returns immediately.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param token Hexadecimal device token
|
||||
*/
|
||||
void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const char *token);
|
||||
|
||||
/**
|
||||
* Create the event emitter that is used to receive events.
|
||||
*
|
||||
@@ -4114,7 +4169,6 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
|
||||
* true if the Webxdc should get full internet access, including Webrtc.
|
||||
* currently, this is only true for encrypted Webxdc's in the self chat
|
||||
* that have requested internet access in the manifest.
|
||||
* this is useful for development and maybe for internal integrations at some point.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The webxdc instance.
|
||||
@@ -4397,6 +4451,9 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
||||
* Currently, the following types are defined:
|
||||
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
|
||||
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
|
||||
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
|
||||
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
|
||||
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
|
||||
*
|
||||
* Even when you display an icon,
|
||||
* you should still display the text of the informational message using dc_msg_get_text()
|
||||
@@ -4423,6 +4480,7 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
|
||||
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
|
||||
#define DC_INFO_PROTECTION_ENABLED 11
|
||||
#define DC_INFO_PROTECTION_DISABLED 12
|
||||
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
|
||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||
|
||||
/**
|
||||
@@ -5068,6 +5126,15 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
*/
|
||||
int dc_contact_is_verified (dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Returns whether contact is a bot.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return 0 if the contact is not a bot, 1 otherwise.
|
||||
*/
|
||||
int dc_contact_is_bot (dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Return the contact ID that verified a contact.
|
||||
@@ -5283,52 +5350,6 @@ uint32_t dc_lot_get_id (const dc_lot_t* lot);
|
||||
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_reactions_t
|
||||
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
|
||||
*
|
||||
* An object representing all reactions for a single message.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns array of contacts which reacted to the given message.
|
||||
*
|
||||
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
|
||||
* @memberof dc_reactions_t
|
||||
* @param reactions The object containing message reactions.
|
||||
* @return array of contact IDs. Use dc_array_get_cnt() to get array length and
|
||||
* dc_array_get_id() to get the IDs. Should be freed using `dc_array_unref()` after usage.
|
||||
*/
|
||||
dc_array_t* dc_reactions_get_contacts(dc_reactions_t* reactions);
|
||||
|
||||
|
||||
/**
|
||||
* Returns a string containing space-separated reactions of a single contact.
|
||||
*
|
||||
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
|
||||
* @memberof dc_reactions_t
|
||||
* @param reactions The object containing message reactions.
|
||||
* @param contact_id ID of the contact.
|
||||
* @return Space-separated list of emoji sequences, which could be empty.
|
||||
* Returned string should not be modified and should be freed
|
||||
* with dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_reactions_get_by_contact_id(dc_reactions_t* reactions, uint32_t contact_id);
|
||||
|
||||
|
||||
/**
|
||||
* Frees an object containing message reactions.
|
||||
*
|
||||
* Reactions objects are created by dc_get_msg_reactions().
|
||||
*
|
||||
* @deprecated 2023-11-27
|
||||
* @memberof dc_reactions_t
|
||||
* @param reactions The object to free.
|
||||
* If NULL is given, nothing is done.
|
||||
*/
|
||||
void dc_reactions_unref (dc_reactions_t* reactions);
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_MSG DC_MSG
|
||||
*
|
||||
@@ -6019,10 +6040,12 @@ void dc_event_unref(dc_event_t* event);
|
||||
* Downloading a bunch of messages just finished. This is an
|
||||
* event to allow the UI to only show one notification per message bunch,
|
||||
* instead of cluttering the user with many notifications.
|
||||
* For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before.
|
||||
* UI may store #DC_EVENT_INCOMING_MSG events
|
||||
* and display notifications for all messages at once
|
||||
* when this event arrives.
|
||||
*
|
||||
* @param data1 0
|
||||
* @param data2 (char*) msg_ids, a json object with the message ids.
|
||||
* @param data2 0
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_MSG_BUNCH 2006
|
||||
|
||||
@@ -6206,6 +6229,18 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_SELFAVATAR_CHANGED 2110
|
||||
|
||||
|
||||
/**
|
||||
* A multi-device synced config value changed. Maybe the app needs to refresh smth. For uniformity
|
||||
* this is emitted on the source device too. The value isn't reported, otherwise it would be logged
|
||||
* which might not be good for privacy. You can get the new value with
|
||||
* `dc_get_config(context, data2)`.
|
||||
*
|
||||
* @param data1 0
|
||||
* @param data2 (char*) Configuration key.
|
||||
*/
|
||||
#define DC_EVENT_CONFIG_SYNCED 2111
|
||||
|
||||
|
||||
/**
|
||||
* webxdc status update received.
|
||||
* To get the received status update, use dc_get_webxdc_status_updates() with
|
||||
@@ -6230,6 +6265,33 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
|
||||
|
||||
/**
|
||||
* Tells that the Background fetch was completed (or timed out).
|
||||
*
|
||||
* This event acts as a marker, when you reach this event you can be sure
|
||||
* that all events emitted during the background fetch were processed.
|
||||
*
|
||||
* This event is only emitted by the account manager
|
||||
*/
|
||||
|
||||
#define DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE 2200
|
||||
|
||||
/**
|
||||
* Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||
*
|
||||
* Sometimes this is emitted together with `DC_EVENT_CHATLIST_ITEM_CHANGED`.
|
||||
*/
|
||||
|
||||
#define DC_EVENT_CHATLIST_CHANGED 2300
|
||||
|
||||
/**
|
||||
* Inform that all or a single chat list item changed and needs to be rerendered
|
||||
* If `chat_id` is set to 0, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
|
||||
*
|
||||
* @param data1 (int) chat_id chat id of chatlist item to be rerendered, if chat_id = 0 all (cached & visible) items need to be rerendered
|
||||
*/
|
||||
|
||||
#define DC_EVENT_CHATLIST_ITEM_CHANGED 2301
|
||||
|
||||
/**
|
||||
* @}
|
||||
@@ -6998,6 +7060,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "You added member %1$s."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the added member's name.
|
||||
#define DC_STR_ADD_MEMBER_BY_YOU 128
|
||||
|
||||
/// "Member %1$s added by %2$s."
|
||||
@@ -7219,6 +7283,37 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as the first info messages in newly created groups.
|
||||
#define DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE 172
|
||||
|
||||
/// "Member %1$s added."
|
||||
///
|
||||
/// Used as info messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the added member's name.
|
||||
#define DC_STR_MESSAGE_ADD_MEMBER 173
|
||||
|
||||
/// "Your email provider %1$s requires end-to-end encryption which is not setup yet."
|
||||
///
|
||||
/// Used as info messages when a message cannot be sent because it cannot be encrypted.
|
||||
///
|
||||
/// `%1$s` will be replaced by the provider's domain.
|
||||
#define DC_STR_INVALID_UNENCRYPTED_MAIL 174
|
||||
|
||||
/// "You reacted %1$s to '%2$s'"
|
||||
///
|
||||
/// `%1$s` will be replaced by the reaction, usually an emoji
|
||||
/// `%2$s` will be replaced by the summary of the message the reaction refers to
|
||||
///
|
||||
/// Used in summaries.
|
||||
#define DC_STR_YOU_REACTED 176
|
||||
|
||||
/// "%1$s reacted %2$s to '%3$s'"
|
||||
///
|
||||
/// `%1$s` will be replaced by the name the contact who reacted
|
||||
/// `%2$s` will be replaced by the reaction, usually an emoji
|
||||
/// `%3$s` will be replaced by the summary of the message the reaction refers to
|
||||
///
|
||||
/// Used in summaries.
|
||||
#define DC_STR_REACTED_BY 177
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
non_upper_case_globals,
|
||||
non_upper_case_globals,
|
||||
non_camel_case_types,
|
||||
clippy::missing_safety_doc,
|
||||
clippy::expect_fun_call
|
||||
)]
|
||||
@@ -26,15 +24,13 @@ use anyhow::Context as _;
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, ContactId, Origin};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::context::{Context, ContextBuilder};
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::imex::BackupProvider;
|
||||
use deltachat::key::preconfigure_keypair;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
@@ -69,8 +65,6 @@ const DC_GCM_INFO_ONLY: u32 = 0x02;
|
||||
/// Struct representing the deltachat context.
|
||||
pub type dc_context_t = Context;
|
||||
|
||||
pub type dc_reactions_t = Reactions;
|
||||
|
||||
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
|
||||
|
||||
fn block_on<T>(fut: T) -> T::Output
|
||||
@@ -104,12 +98,11 @@ pub unsafe extern "C" fn dc_context_new(
|
||||
let ctx = if blobdir.is_null() || *blobdir == 0 {
|
||||
// generate random ID as this functionality is not yet available on the C-api.
|
||||
let id = rand::thread_rng().gen();
|
||||
block_on(Context::new(
|
||||
as_path(dbfile),
|
||||
id,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
))
|
||||
block_on(
|
||||
ContextBuilder::new(as_path(dbfile).to_path_buf())
|
||||
.with_id(id)
|
||||
.open(),
|
||||
)
|
||||
} else {
|
||||
eprintln!("blobdir can not be defined explicitly anymore");
|
||||
return ptr::null_mut();
|
||||
@@ -133,12 +126,11 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
|
||||
}
|
||||
|
||||
let id = rand::thread_rng().gen();
|
||||
match block_on(Context::new_closed(
|
||||
as_path(dbfile),
|
||||
id,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
)) {
|
||||
match block_on(
|
||||
ContextBuilder::new(as_path(dbfile).to_path_buf())
|
||||
.with_id(id)
|
||||
.build(),
|
||||
) {
|
||||
Ok(context) => Box::into_raw(Box::new(context)),
|
||||
Err(err) => {
|
||||
eprintln!("failed to create context: {err:#}");
|
||||
@@ -387,7 +379,7 @@ pub unsafe extern "C" fn dc_get_connectivity(context: *const dc_context_t) -> li
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(async move { ctx.get_connectivity().await as u32 as libc::c_int })
|
||||
block_on(ctx.get_connectivity()) as u32 as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -410,6 +402,16 @@ pub unsafe extern "C" fn dc_get_connectivity_html(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_push_state(context: *const dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_push_state()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(ctx.push_state()) as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_all_work_done(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
@@ -556,8 +558,12 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::SecurejoinJoinerProgress { .. } => 2061,
|
||||
EventType::ConnectivityChanged => 2100,
|
||||
EventType::SelfavatarChanged => 2110,
|
||||
EventType::ConfigSynced { .. } => 2111,
|
||||
EventType::WebxdcStatusUpdate { .. } => 2120,
|
||||
EventType::WebxdcInstanceDeleted { .. } => 2121,
|
||||
EventType::AccountsBackgroundFetchDone => 2200,
|
||||
EventType::ChatlistChanged => 2300,
|
||||
EventType::ChatlistItemChanged { .. } => 2301,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,8 +589,11 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::Error(_)
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::ConfigSynced { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ErrorSelfNotInGroup(_) => 0,
|
||||
| EventType::ErrorSelfNotInGroup(_)
|
||||
| EventType::AccountsBackgroundFetchDone => 0,
|
||||
EventType::ChatlistChanged => 0,
|
||||
EventType::MsgsChanged { chat_id, .. }
|
||||
| EventType::ReactionsChanged { chat_id, .. }
|
||||
| EventType::IncomingMsg { chat_id, .. }
|
||||
@@ -609,6 +618,9 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
}
|
||||
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::ChatlistItemChanged { chat_id } => {
|
||||
chat_id.unwrap_or_default().to_u32() as libc::c_int
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,7 +655,11 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::SelfavatarChanged => 0,
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ConfigSynced { .. } => 0,
|
||||
EventType::ChatModified(_) => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
@@ -705,7 +721,11 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::WebxdcStatusUpdate { .. }
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatEphemeralTimerModified { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ChatlistChanged => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
comment.to_c_string().unwrap_or_default().into_raw()
|
||||
@@ -717,11 +737,10 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
let data2 = file.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::IncomingMsgBunch { msg_ids } => serde_json::to_string(msg_ids)
|
||||
.unwrap_or_default()
|
||||
.to_c_string()
|
||||
.unwrap_or_default()
|
||||
.into_raw(),
|
||||
EventType::ConfigSynced { key } => {
|
||||
let data2 = key.to_string().to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1001,49 +1020,6 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_reaction(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
reaction: *const libc::c_char,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_send_reaction()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
send_reaction(ctx, MsgId::new(msg_id), &to_string_lossy(reaction))
|
||||
.await
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.unwrap_or_log_default(ctx, "Failed to send reaction")
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_msg_reactions(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
) -> *mut dc_reactions_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_msg_reactions()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
|
||||
.context("failed dc_get_msg_reactions() call")
|
||||
.log_err(ctx)
|
||||
{
|
||||
reactions
|
||||
} else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
|
||||
Box::into_raw(Box::new(reactions))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1087,6 +1063,43 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates(
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_webxdc_integration(
|
||||
context: *mut dc_context_t,
|
||||
file: *const libc::c_char,
|
||||
) {
|
||||
if context.is_null() || file.is_null() {
|
||||
eprintln!("ignoring careless call to dc_set_webxdc_integration()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(ctx.set_webxdc_integration(&to_string_lossy(file)))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_init_webxdc_integration(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_init_webxdc_integration()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let chat_id = if chat_id == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(ChatId::new(chat_id))
|
||||
};
|
||||
|
||||
block_on(ctx.init_webxdc_integration(chat_id))
|
||||
.log_err(ctx)
|
||||
.map(|msg_id| msg_id.map(|id| id.to_u32()).unwrap_or_default())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_draft(
|
||||
context: *mut dc_context_t,
|
||||
@@ -2032,7 +2045,7 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
|
||||
);
|
||||
message::Message::default()
|
||||
} else {
|
||||
error!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
|
||||
warn!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
}
|
||||
@@ -4125,6 +4138,15 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_is_bot(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_is_bot()");
|
||||
return 0;
|
||||
}
|
||||
(*contact).contact.is_bot() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> u32 {
|
||||
if contact.is_null() {
|
||||
@@ -4221,45 +4243,6 @@ pub unsafe extern "C" fn dc_lot_get_timestamp(lot: *mut dc_lot_t) -> i64 {
|
||||
lot.get_timestamp()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_reactions_get_contacts(
|
||||
reactions: *mut dc_reactions_t,
|
||||
) -> *mut dc_array::dc_array_t {
|
||||
if reactions.is_null() {
|
||||
eprintln!("ignoring careless call to dc_reactions_get_contacts()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let reactions = &*reactions;
|
||||
let array: dc_array_t = reactions.contacts().into();
|
||||
|
||||
Box::into_raw(Box::new(array))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_reactions_get_by_contact_id(
|
||||
reactions: *mut dc_reactions_t,
|
||||
contact_id: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if reactions.is_null() {
|
||||
eprintln!("ignoring careless call to dc_reactions_get_by_contact_id()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let reactions = &*reactions;
|
||||
reactions.get(ContactId::new(contact_id)).as_str().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_reactions_unref(reactions: *mut dc_reactions_t) {
|
||||
if reactions.is_null() {
|
||||
eprintln!("ignoring careless call to dc_reactions_unref()");
|
||||
return;
|
||||
}
|
||||
|
||||
drop(Box::from_raw(reactions));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
|
||||
libc::free(s as *mut _)
|
||||
@@ -4460,19 +4443,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
trait ResultNullableExt<T> {
|
||||
fn into_raw(self) -> *mut T;
|
||||
}
|
||||
|
||||
impl<T, E> ResultNullableExt<T> for Result<T, E> {
|
||||
fn into_raw(self) -> *mut T {
|
||||
match self {
|
||||
Ok(t) => Box::into_raw(Box::new(t)),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> Vec<MsgId> {
|
||||
let ids = unsafe { std::slice::from_raw_parts(msg_ids, msg_cnt as usize) };
|
||||
let msg_ids: Vec<MsgId> = ids
|
||||
@@ -4828,16 +4798,6 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
|
||||
Box::into_raw(Box::new(array))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_all_work_done(accounts: *mut dc_accounts_t) -> libc::c_int {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_all_work_done()");
|
||||
return 0;
|
||||
}
|
||||
let accounts = &*accounts;
|
||||
block_on(async move { accounts.read().await.all_work_done().await as libc::c_int })
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
@@ -4882,6 +4842,49 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
|
||||
block_on(async move { accounts.write().await.maybe_network_lost().await });
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_background_fetch(
|
||||
accounts: *mut dc_accounts_t,
|
||||
timeout_in_seconds: u64,
|
||||
) -> libc::c_int {
|
||||
if accounts.is_null() || timeout_in_seconds <= 2 {
|
||||
eprintln!("ignoring careless call to dc_accounts_background_fetch()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(async move {
|
||||
let accounts = accounts.read().await;
|
||||
accounts
|
||||
.background_fetch(Duration::from_secs(timeout_in_seconds))
|
||||
.await;
|
||||
});
|
||||
1
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_set_push_device_token(
|
||||
accounts: *mut dc_accounts_t,
|
||||
token: *const libc::c_char,
|
||||
) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_set_push_device_token()");
|
||||
return;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let token = to_string_lossy(token);
|
||||
|
||||
block_on(async move {
|
||||
let mut accounts = accounts.write().await;
|
||||
if let Err(err) = accounts.set_push_device_token(&token).await {
|
||||
accounts.emit_event(EventType::Error(format!(
|
||||
"Failed to set notify token: {err:#}."
|
||||
)));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
accounts: *mut dc_accounts_t,
|
||||
@@ -4919,7 +4922,9 @@ mod jsonrpc {
|
||||
}
|
||||
|
||||
let account_manager = &*account_manager;
|
||||
let cmd_api = deltachat_jsonrpc::api::CommandApi::from_arc(account_manager.inner.clone());
|
||||
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
|
||||
account_manager.inner.clone(),
|
||||
));
|
||||
|
||||
let (request_handle, receiver) = RpcClient::new();
|
||||
let handle = RpcSession::new(request_handle, cmd_api);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.132.1"
|
||||
version = "1.137.4"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[[bin]]
|
||||
name = "deltachat-jsonrpc-server"
|
||||
@@ -17,24 +18,24 @@ deltachat = { path = ".." }
|
||||
num-traits = "0.2"
|
||||
schemars = "0.8.13"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tempfile = "3.9.0"
|
||||
tempfile = "3.10.1"
|
||||
log = "0.4"
|
||||
async-channel = { version = "2.0.0" }
|
||||
futures = { version = "0.3.30" }
|
||||
serde_json = "1"
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
|
||||
tokio = { version = "1.33.0" }
|
||||
tokio = { version = "1.37.0" }
|
||||
sanitize-filename = "0.5"
|
||||
walkdir = "2.3.3"
|
||||
base64 = "0.21"
|
||||
walkdir = "2.5.0"
|
||||
base64 = "0.22"
|
||||
|
||||
# optional dependencies
|
||||
axum = { version = "0.7", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.10.0", optional = true }
|
||||
env_logger = { version = "0.11.3", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.33.0", features = ["full", "rt-multi-thread"] }
|
||||
tokio = { version = "1.37.0", features = ["full", "rt-multi-thread"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
@@ -28,6 +29,7 @@ use deltachat::reaction::{get_msg_reactions, send_reaction};
|
||||
use deltachat::securejoin;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::EventEmitter;
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
@@ -62,14 +64,14 @@ use crate::api::types::qr::QrObject;
|
||||
struct AccountState {
|
||||
/// The Qr code for current [`CommandApi::provide_backup`] call.
|
||||
///
|
||||
/// If there currently is a call to [`CommandApi::provide_backup`] this will be
|
||||
/// `Pending` or `Ready`, otherwise `NoProvider`.
|
||||
backup_provider_qr: watch::Sender<ProviderQr>,
|
||||
/// If there is currently is a call to [`CommandApi::provide_backup`] this will be
|
||||
/// `Some`, otherwise `None`.
|
||||
backup_provider_qr: watch::Sender<Option<Qr>>,
|
||||
}
|
||||
|
||||
impl Default for AccountState {
|
||||
fn default() -> Self {
|
||||
let (tx, _rx) = watch::channel(ProviderQr::NoProvider);
|
||||
let tx = watch::Sender::new(None);
|
||||
Self {
|
||||
backup_provider_qr: tx,
|
||||
}
|
||||
@@ -80,21 +82,30 @@ impl Default for AccountState {
|
||||
pub struct CommandApi {
|
||||
pub(crate) accounts: Arc<RwLock<Accounts>>,
|
||||
|
||||
/// Receiver side of the event channel.
|
||||
///
|
||||
/// Events from it can be received by calling `get_next_event` method.
|
||||
event_emitter: Arc<EventEmitter>,
|
||||
|
||||
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
|
||||
}
|
||||
|
||||
impl CommandApi {
|
||||
pub fn new(accounts: Accounts) -> Self {
|
||||
let event_emitter = Arc::new(accounts.get_event_emitter());
|
||||
CommandApi {
|
||||
accounts: Arc::new(RwLock::new(accounts)),
|
||||
event_emitter,
|
||||
states: Arc::new(Mutex::new(BTreeMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
|
||||
pub async fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
|
||||
let event_emitter = Arc::new(accounts.read().await.get_event_emitter());
|
||||
CommandApi {
|
||||
accounts,
|
||||
event_emitter,
|
||||
states: Arc::new(Mutex::new(BTreeMap::new())),
|
||||
}
|
||||
}
|
||||
@@ -123,21 +134,13 @@ impl CommandApi {
|
||||
.with_state(account_id, |state| state.backup_provider_qr.subscribe())
|
||||
.await;
|
||||
|
||||
let val: ProviderQr = receiver.borrow_and_update().clone();
|
||||
match val {
|
||||
ProviderQr::NoProvider => bail!("No backup being provided"),
|
||||
ProviderQr::Pending => loop {
|
||||
if receiver.changed().await.is_err() {
|
||||
bail!("No backup being provided (account state dropped)");
|
||||
}
|
||||
let val: ProviderQr = receiver.borrow().clone();
|
||||
match val {
|
||||
ProviderQr::NoProvider => bail!("No backup being provided"),
|
||||
ProviderQr::Pending => continue,
|
||||
ProviderQr::Ready(qr) => break Ok(qr),
|
||||
};
|
||||
},
|
||||
ProviderQr::Ready(qr) => Ok(qr),
|
||||
loop {
|
||||
if let Some(qr) = receiver.borrow_and_update().clone() {
|
||||
return Ok(qr);
|
||||
}
|
||||
if receiver.changed().await.is_err() {
|
||||
bail!("No backup being provided (account state dropped)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,8 +168,7 @@ impl CommandApi {
|
||||
|
||||
/// Get the next event.
|
||||
async fn get_next_event(&self) -> Result<Event> {
|
||||
let event_emitter = self.accounts.read().await.get_event_emitter();
|
||||
event_emitter
|
||||
self.event_emitter
|
||||
.recv()
|
||||
.await
|
||||
.map(|event| event.into())
|
||||
@@ -231,13 +233,27 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout.
|
||||
/// Process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||
self.accounts
|
||||
.write()
|
||||
.await
|
||||
.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Methods that work on individual accounts
|
||||
// ---------------------------------------------
|
||||
|
||||
/// Starts background tasks for a single account.
|
||||
async fn start_io(&self, account_id: u32) -> Result<()> {
|
||||
let mut ctx = self.get_context(account_id).await?;
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -311,6 +327,11 @@ impl CommandApi {
|
||||
ctx.get_info().await
|
||||
}
|
||||
|
||||
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?;
|
||||
@@ -383,7 +404,7 @@ impl CommandApi {
|
||||
/// Configures this account with the currently set parameters.
|
||||
/// Setup the credential config before calling this.
|
||||
async fn configure(&self, account_id: u32) -> Result<()> {
|
||||
let mut ctx = self.get_context(account_id).await?;
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.stop_io().await;
|
||||
let result = ctx.configure().await;
|
||||
if result.is_err() {
|
||||
@@ -678,8 +699,7 @@ impl CommandApi {
|
||||
/// the Verified-Group-Invite protocol is offered in the QR code;
|
||||
/// works for protected groups as well as for normal groups.
|
||||
/// If not set, the Setup-Contact protocol is offered in the QR code.
|
||||
/// See https://securejoin.readthedocs.io/en/latest/new.html
|
||||
/// for details about both protocols.
|
||||
/// See https://securejoin.delta.chat/ for details about both protocols.
|
||||
///
|
||||
/// return format: `[code, svg]`
|
||||
async fn get_chat_securejoin_qr_code_svg(
|
||||
@@ -707,8 +727,7 @@ impl CommandApi {
|
||||
///
|
||||
/// Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||
///
|
||||
/// See https://securejoin.readthedocs.io/en/latest/new.html
|
||||
/// for details about both protocols.
|
||||
/// See https://securejoin.delta.chat/ for details about both protocols.
|
||||
///
|
||||
/// **qr**: The text of the scanned QR code. Typically, the same string as given
|
||||
/// to `check_qr()`.
|
||||
@@ -896,19 +915,35 @@ impl CommandApi {
|
||||
.to_u32())
|
||||
}
|
||||
|
||||
// for now only text messages, because we only used text messages in desktop thusfar
|
||||
/// Add a message to the device-chat.
|
||||
/// Device-messages usually contain update information
|
||||
/// and some hints that are added during the program runs, multi-device etc.
|
||||
/// The device-message may be defined by a label;
|
||||
/// if a message with the same label was added or skipped before,
|
||||
/// the message is not added again, even if the message was deleted in between.
|
||||
/// If needed, the device-chat is created before.
|
||||
///
|
||||
/// Sends the `MsgsChanged` event on success.
|
||||
///
|
||||
/// Setting msg to None will prevent the device message with this label from being added in the future.
|
||||
async fn add_device_message(
|
||||
&self,
|
||||
account_id: u32,
|
||||
label: String,
|
||||
text: String,
|
||||
) -> Result<u32> {
|
||||
msg: Option<MessageData>,
|
||||
) -> Result<Option<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(text);
|
||||
let message_id =
|
||||
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
|
||||
Ok(message_id.to_u32())
|
||||
if let Some(msg) = msg {
|
||||
let mut message = msg.create_message(&ctx).await?;
|
||||
let message_id =
|
||||
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut message)).await?;
|
||||
if !message_id.is_unset() {
|
||||
return Ok(Some(message_id.to_u32()));
|
||||
}
|
||||
} else {
|
||||
deltachat::chat::add_device_msg(&ctx, Some(&label), None).await?;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Mark all messages in a chat as _noticed_.
|
||||
@@ -1062,9 +1097,12 @@ impl CommandApi {
|
||||
.collect::<Vec<JSONRPCMessageListItem>>())
|
||||
}
|
||||
|
||||
async fn get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
|
||||
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
MessageObject::from_message_id(&ctx, message_id).await
|
||||
let msg_id = MsgId::new(msg_id);
|
||||
MessageObject::from_msg_id(&ctx, msg_id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
|
||||
}
|
||||
|
||||
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
|
||||
@@ -1084,7 +1122,7 @@ impl CommandApi {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut messages: HashMap<u32, MessageLoadResult> = HashMap::new();
|
||||
for message_id in message_ids {
|
||||
let message_result = MessageObject::from_message_id(&ctx, message_id).await;
|
||||
let message_result = MessageObject::from_msg_id(&ctx, MsgId::new(message_id)).await;
|
||||
messages.insert(
|
||||
message_id,
|
||||
match message_result {
|
||||
@@ -1531,20 +1569,21 @@ impl CommandApi {
|
||||
/// Returns once a remote device has retrieved the backup, or is cancelled.
|
||||
async fn provide_backup(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
self.with_state(account_id, |state| {
|
||||
state.backup_provider_qr.send_replace(ProviderQr::Pending);
|
||||
})
|
||||
.await;
|
||||
|
||||
let provider = imex::BackupProvider::prepare(&ctx).await?;
|
||||
self.with_state(account_id, |state| {
|
||||
state
|
||||
.backup_provider_qr
|
||||
.send_replace(ProviderQr::Ready(provider.qr()));
|
||||
state.backup_provider_qr.send_replace(Some(provider.qr()));
|
||||
})
|
||||
.await;
|
||||
|
||||
provider.await
|
||||
let res = provider.await;
|
||||
|
||||
self.with_state(account_id, |state| {
|
||||
state.backup_provider_qr.send_replace(None);
|
||||
})
|
||||
.await;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// Returns the text of the QR code for the running [`CommandApi::provide_backup`].
|
||||
@@ -1552,11 +1591,17 @@ impl CommandApi {
|
||||
/// This QR code text can be used in [`CommandApi::get_backup`] on a second device to
|
||||
/// retrieve the backup and setup this second device.
|
||||
///
|
||||
/// This call will fail if there is currently no concurrent call to
|
||||
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
|
||||
/// ready.
|
||||
/// This call will block until the QR code is ready,
|
||||
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||
/// but will fail after 10 seconds to avoid deadlocks.
|
||||
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
|
||||
let qr = self.inner_get_backup_qr(account_id).await?;
|
||||
let qr = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
self.inner_get_backup_qr(account_id),
|
||||
)
|
||||
.await
|
||||
.context("Backup provider did not start in time")?
|
||||
.context("Failed to get backup QR code")?;
|
||||
qr::format_backup(&qr)
|
||||
}
|
||||
|
||||
@@ -1565,14 +1610,20 @@ impl CommandApi {
|
||||
/// This QR code can be used in [`CommandApi::get_backup`] on a second device to
|
||||
/// retrieve the backup and setup this second device.
|
||||
///
|
||||
/// This call will fail if there is currently no concurrent call to
|
||||
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
|
||||
/// ready.
|
||||
/// This call will block until the QR code is ready,
|
||||
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||
/// but will fail after 10 seconds to avoid deadlocks.
|
||||
///
|
||||
/// Returns the QR code rendered as an SVG image.
|
||||
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let qr = self.inner_get_backup_qr(account_id).await?;
|
||||
let qr = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
self.inner_get_backup_qr(account_id),
|
||||
)
|
||||
.await
|
||||
.context("Backup provider did not start in time")?
|
||||
.context("Failed to get backup QR code")?;
|
||||
generate_backup_qr(&ctx, &qr).await
|
||||
}
|
||||
|
||||
@@ -1582,6 +1633,9 @@ impl CommandApi {
|
||||
/// the current device.
|
||||
///
|
||||
/// Can be cancelled by stopping the ongoing process.
|
||||
///
|
||||
/// Do not forget to call start_io on the account after a successful import,
|
||||
/// otherwise it will not connect to the email server.
|
||||
async fn get_backup(&self, account_id: u32, qr_text: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let qr = qr::check_qr(&ctx, &qr_text).await?;
|
||||
@@ -1808,38 +1862,7 @@ impl CommandApi {
|
||||
|
||||
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut message = Message::new(if let Some(viewtype) = data.viewtype {
|
||||
viewtype.into()
|
||||
} else if data.file.is_some() {
|
||||
Viewtype::File
|
||||
} else {
|
||||
Viewtype::Text
|
||||
});
|
||||
message.set_text(data.text.unwrap_or_default());
|
||||
if data.html.is_some() {
|
||||
message.set_html(data.html);
|
||||
}
|
||||
if data.override_sender_name.is_some() {
|
||||
message.set_override_sender_name(data.override_sender_name);
|
||||
}
|
||||
if let Some(file) = data.file {
|
||||
message.set_file(file, None);
|
||||
}
|
||||
if let Some((latitude, longitude)) = data.location {
|
||||
message.set_location(latitude, longitude);
|
||||
}
|
||||
if let Some(id) = data.quoted_message_id {
|
||||
message
|
||||
.set_quote(
|
||||
&ctx,
|
||||
Some(
|
||||
&Message::load_from_db(&ctx, MsgId::new(id))
|
||||
.await
|
||||
.context("message to quote could not be loaded")?,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let mut message = data.create_message(&ctx).await?;
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
||||
.await?
|
||||
.to_u32();
|
||||
@@ -2038,11 +2061,9 @@ impl CommandApi {
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
||||
.await?
|
||||
.to_u32();
|
||||
let message = MessageObject::from_message_id(&ctx, msg_id).await?;
|
||||
Ok((msg_id, message))
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
|
||||
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
|
||||
Ok((msg_id.to_u32(), message))
|
||||
}
|
||||
|
||||
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
|
||||
@@ -2121,13 +2142,6 @@ async fn set_config(
|
||||
value,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match key {
|
||||
"sentbox_watch" | "mvbox_move" | "only_fetch_mvbox" => {
|
||||
ctx.restart_io_if_running().await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2143,15 +2157,3 @@ async fn get_config(
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a QR code for a BackupProvider is currently available.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Debug)]
|
||||
enum ProviderQr {
|
||||
/// There is no provider, asking for a QR is an error.
|
||||
NoProvider,
|
||||
/// There is a provider, the QR code is pending.
|
||||
Pending,
|
||||
/// There is a provider and QR code.
|
||||
Ready(Qr),
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ pub struct ContactObject {
|
||||
/// the contact's last seen timestamp
|
||||
last_seen: i64,
|
||||
was_seen_recently: bool,
|
||||
|
||||
/// If the contact is a bot.
|
||||
is_bot: bool,
|
||||
}
|
||||
|
||||
impl ContactObject {
|
||||
@@ -80,6 +83,7 @@ impl ContactObject {
|
||||
verifier_id,
|
||||
last_seen: contact.last_seen(),
|
||||
was_seen_recently: contact.was_seen_recently(),
|
||||
is_bot: contact.is_bot(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,55 +28,37 @@ pub enum EventType {
|
||||
///
|
||||
/// This event should *not* be reported to the end-user using a popup or something like
|
||||
/// that.
|
||||
Info {
|
||||
msg: String,
|
||||
},
|
||||
Info { msg: String },
|
||||
|
||||
/// Emitted when SMTP connection is established and login was successful.
|
||||
SmtpConnected {
|
||||
msg: String,
|
||||
},
|
||||
SmtpConnected { msg: String },
|
||||
|
||||
/// Emitted when IMAP connection is established and login was successful.
|
||||
ImapConnected {
|
||||
msg: String,
|
||||
},
|
||||
ImapConnected { msg: String },
|
||||
|
||||
/// Emitted when a message was successfully sent to the SMTP server.
|
||||
SmtpMessageSent {
|
||||
msg: String,
|
||||
},
|
||||
SmtpMessageSent { msg: String },
|
||||
|
||||
/// Emitted when an IMAP message has been marked as deleted
|
||||
ImapMessageDeleted {
|
||||
msg: String,
|
||||
},
|
||||
ImapMessageDeleted { msg: String },
|
||||
|
||||
/// Emitted when an IMAP message has been moved
|
||||
ImapMessageMoved {
|
||||
msg: String,
|
||||
},
|
||||
ImapMessageMoved { msg: String },
|
||||
|
||||
/// Emitted before going into IDLE on the Inbox folder.
|
||||
ImapInboxIdle,
|
||||
|
||||
/// Emitted when an new file in the $BLOBDIR was created
|
||||
NewBlobFile {
|
||||
file: String,
|
||||
},
|
||||
NewBlobFile { file: String },
|
||||
|
||||
/// Emitted when an file in the $BLOBDIR was deleted
|
||||
DeletedBlobFile {
|
||||
file: String,
|
||||
},
|
||||
DeletedBlobFile { file: String },
|
||||
|
||||
/// The library-user should write a warning string to the log.
|
||||
///
|
||||
/// This event should *not* be reported to the end-user using a popup or something like
|
||||
/// that.
|
||||
Warning {
|
||||
msg: String,
|
||||
},
|
||||
Warning { msg: String },
|
||||
|
||||
/// The library-user should report an error to the end-user.
|
||||
///
|
||||
@@ -88,18 +70,14 @@ pub enum EventType {
|
||||
/// it might be better to delay showing these events until the function has really
|
||||
/// failed (returned false). It should be sufficient to report only the *last* error
|
||||
/// in a messasge box then.
|
||||
Error {
|
||||
msg: String,
|
||||
},
|
||||
Error { msg: String },
|
||||
|
||||
/// An action cannot be performed because the user is not in the group.
|
||||
/// Reported eg. after a call to
|
||||
/// setChatName(), setChatProfileImage(),
|
||||
/// addContactToChat(), removeContactFromChat(),
|
||||
/// and messages sending functions.
|
||||
ErrorSelfNotInGroup {
|
||||
msg: String,
|
||||
},
|
||||
ErrorSelfNotInGroup { msg: String },
|
||||
|
||||
/// Messages or chats changed. One or more messages or chats changed for various
|
||||
/// reasons in the database:
|
||||
@@ -110,10 +88,7 @@ pub enum EventType {
|
||||
/// `chatId` is set if only a single chat is affected by the changes, otherwise 0.
|
||||
/// `msgId` is set if only a single message is affected by the changes, otherwise 0.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgsChanged {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
MsgsChanged { chat_id: u32, msg_id: u32 },
|
||||
|
||||
/// Reactions for the message changed.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -126,60 +101,39 @@ pub enum EventType {
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
|
||||
/// There is no extra #DC_EVENT_MSGS_CHANGED event sent together with this event.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingMsg {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
IncomingMsg { chat_id: u32, msg_id: u32 },
|
||||
|
||||
/// Downloading a bunch of messages just finished. This is an experimental
|
||||
/// Downloading a bunch of messages just finished. This is an
|
||||
/// event to allow the UI to only show one notification per message bunch,
|
||||
/// instead of cluttering the user with many notifications.
|
||||
///
|
||||
/// msg_ids contains the message ids.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingMsgBunch {
|
||||
msg_ids: Vec<u32>,
|
||||
},
|
||||
IncomingMsgBunch,
|
||||
|
||||
/// Messages were seen or noticed.
|
||||
/// chat id is always set.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgsNoticed {
|
||||
chat_id: u32,
|
||||
},
|
||||
MsgsNoticed { chat_id: u32 },
|
||||
|
||||
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||
/// DC_STATE_OUT_DELIVERED, see `Message.state`.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgDelivered {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
MsgDelivered { chat_id: u32, msg_id: u32 },
|
||||
|
||||
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
||||
/// DC_STATE_OUT_FAILED, see `Message.state`.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgFailed {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
MsgFailed { chat_id: u32, msg_id: u32 },
|
||||
|
||||
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||
/// DC_STATE_OUT_MDN_RCVD, see `Message.state`.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgRead {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
MsgRead { chat_id: u32, msg_id: u32 },
|
||||
|
||||
/// A single message is deleted.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgDeleted {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
MsgDeleted { chat_id: u32, msg_id: u32 },
|
||||
|
||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
/// Or the verify state of a chat has changed.
|
||||
@@ -189,24 +143,17 @@ pub enum EventType {
|
||||
/// This event does not include ephemeral timer modification, which
|
||||
/// is a separate event.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatModified {
|
||||
chat_id: u32,
|
||||
},
|
||||
ChatModified { chat_id: u32 },
|
||||
|
||||
/// Chat ephemeral timer changed.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatEphemeralTimerModified {
|
||||
chat_id: u32,
|
||||
timer: u32,
|
||||
},
|
||||
ChatEphemeralTimerModified { chat_id: u32, timer: u32 },
|
||||
|
||||
/// Contact(s) created, renamed, blocked or deleted.
|
||||
///
|
||||
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContactsChanged {
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
ContactsChanged { contact_id: Option<u32> },
|
||||
|
||||
/// Location of one or more contact has changed.
|
||||
///
|
||||
@@ -214,9 +161,7 @@ pub enum EventType {
|
||||
/// If the locations of several contacts have been changed,
|
||||
/// this parameter is set to `None`.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
LocationChanged {
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
LocationChanged { contact_id: Option<u32> },
|
||||
|
||||
/// Inform about the configuration progress started by configure().
|
||||
ConfigureProgress {
|
||||
@@ -234,9 +179,7 @@ pub enum EventType {
|
||||
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||
/// @param data2 0
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ImexProgress {
|
||||
progress: usize,
|
||||
},
|
||||
ImexProgress { progress: usize },
|
||||
|
||||
/// A file has been exported. A file has been written by imex().
|
||||
/// This event may be sent multiple times by a single call to imex().
|
||||
@@ -246,9 +189,7 @@ pub enum EventType {
|
||||
///
|
||||
/// @param data2 0
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ImexFileWritten {
|
||||
path: String,
|
||||
},
|
||||
ImexFileWritten { path: String },
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the inviter
|
||||
/// (Alice, the person who shows the QR code).
|
||||
@@ -263,10 +204,7 @@ pub enum EventType {
|
||||
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||
/// 1000=Protocol finished for this contact.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
SecurejoinInviterProgress {
|
||||
contact_id: u32,
|
||||
progress: usize,
|
||||
},
|
||||
SecurejoinInviterProgress { contact_id: u32, progress: usize },
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the joiner
|
||||
/// (Bob, the person who scans the QR code).
|
||||
@@ -277,10 +215,7 @@ pub enum EventType {
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
#[serde(rename_all = "camelCase")]
|
||||
SecurejoinJoinerProgress {
|
||||
contact_id: u32,
|
||||
progress: usize,
|
||||
},
|
||||
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
||||
|
||||
/// The connectivity to the server changed.
|
||||
/// This means that you should refresh the connectivity view
|
||||
@@ -288,8 +223,17 @@ pub enum EventType {
|
||||
/// getConnectivityHtml() for details.
|
||||
ConnectivityChanged,
|
||||
|
||||
/// Deprecated by `ConfigSynced`.
|
||||
SelfavatarChanged,
|
||||
|
||||
/// A multi-device synced config value changed. Maybe the app needs to refresh smth. For
|
||||
/// uniformity this is emitted on the source device too. The value isn't here, otherwise it
|
||||
/// would be logged which might not be good for privacy.
|
||||
ConfigSynced {
|
||||
/// Configuration key.
|
||||
key: String,
|
||||
},
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcStatusUpdate {
|
||||
msg_id: u32,
|
||||
@@ -298,9 +242,23 @@ pub enum EventType {
|
||||
|
||||
/// Inform that a message containing a webxdc instance has been deleted
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcInstanceDeleted {
|
||||
msg_id: u32,
|
||||
},
|
||||
WebxdcInstanceDeleted { msg_id: u32 },
|
||||
|
||||
/// Tells that the Background fetch was completed (or timed out).
|
||||
/// This event acts as a marker, when you reach this event you can be sure
|
||||
/// that all events emitted during the background fetch were processed.
|
||||
///
|
||||
/// This event is only emitted by the account manager
|
||||
AccountsBackgroundFetchDone,
|
||||
/// Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||
///
|
||||
/// Sometimes this is emitted together with `UIChatlistItemChanged`.
|
||||
ChatlistChanged,
|
||||
|
||||
/// Inform that a single chat list item changed and needs to be rerendered.
|
||||
/// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatlistItemChanged { chat_id: Option<u32> },
|
||||
}
|
||||
|
||||
impl From<CoreEventType> for EventType {
|
||||
@@ -336,9 +294,7 @@ impl From<CoreEventType> for EventType {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
|
||||
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
|
||||
},
|
||||
CoreEventType::IncomingMsgBunch => IncomingMsgBunch,
|
||||
CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed {
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
@@ -396,6 +352,9 @@ impl From<CoreEventType> for EventType {
|
||||
},
|
||||
CoreEventType::ConnectivityChanged => ConnectivityChanged,
|
||||
CoreEventType::SelfavatarChanged => SelfavatarChanged,
|
||||
CoreEventType::ConfigSynced { key } => ConfigSynced {
|
||||
key: key.to_string(),
|
||||
},
|
||||
CoreEventType::WebxdcStatusUpdate {
|
||||
msg_id,
|
||||
status_update_serial,
|
||||
@@ -406,6 +365,11 @@ impl From<CoreEventType> for EventType {
|
||||
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::AccountsBackgroundFetchDone => AccountsBackgroundFetchDone,
|
||||
CoreEventType::ChatlistItemChanged { chat_id } => ChatlistItemChanged {
|
||||
chat_id: chat_id.map(|id| id.to_u32()),
|
||||
},
|
||||
CoreEventType::ChatlistChanged => ChatlistChanged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,11 +105,6 @@ enum MessageQuote {
|
||||
}
|
||||
|
||||
impl MessageObject {
|
||||
pub async fn from_message_id(context: &Context, message_id: u32) -> Result<Self> {
|
||||
let msg_id = MsgId::new(message_id);
|
||||
Self::from_msg_id(context, msg_id).await
|
||||
}
|
||||
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
|
||||
@@ -345,6 +340,7 @@ pub enum SystemMessageType {
|
||||
SecurejoinMessage,
|
||||
LocationStreamingEnabled,
|
||||
LocationOnly,
|
||||
InvalidUnencryptedMail,
|
||||
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged,
|
||||
@@ -385,6 +381,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
||||
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
|
||||
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
|
||||
SystemMessage::InvalidUnencryptedMail => SystemMessageType::InvalidUnencryptedMail,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -546,6 +543,44 @@ pub struct MessageData {
|
||||
pub quoted_message_id: Option<u32>,
|
||||
}
|
||||
|
||||
impl MessageData {
|
||||
pub(crate) async fn create_message(self, context: &Context) -> Result<Message> {
|
||||
let mut message = Message::new(if let Some(viewtype) = self.viewtype {
|
||||
viewtype.into()
|
||||
} else if self.file.is_some() {
|
||||
Viewtype::File
|
||||
} else {
|
||||
Viewtype::Text
|
||||
});
|
||||
message.set_text(self.text.unwrap_or_default());
|
||||
if self.html.is_some() {
|
||||
message.set_html(self.html);
|
||||
}
|
||||
if self.override_sender_name.is_some() {
|
||||
message.set_override_sender_name(self.override_sender_name);
|
||||
}
|
||||
if let Some(file) = self.file {
|
||||
message.set_file(file, None);
|
||||
}
|
||||
if let Some((latitude, longitude)) = self.location {
|
||||
message.set_location(latitude, longitude);
|
||||
}
|
||||
if let Some(id) = self.quoted_message_id {
|
||||
message
|
||||
.set_quote(
|
||||
context,
|
||||
Some(
|
||||
&Message::load_from_db(context, MsgId::new(id))
|
||||
.await
|
||||
.context("message to quote could not be loaded")?,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageReadReceipt {
|
||||
|
||||
@@ -53,5 +53,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.132.1"
|
||||
"version": "1.137.4"
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.132.1"
|
||||
version = "1.137.4"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[dependencies]
|
||||
ansi_term = "0.12.1"
|
||||
anyhow = "1"
|
||||
deltachat = { path = "..", features = ["internals"]}
|
||||
dirs = "5"
|
||||
log = "0.4.20"
|
||||
log = "0.4.21"
|
||||
pretty_env_logger = "0.5"
|
||||
rusqlite = "0.30"
|
||||
rustyline = "12"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
rusqlite = "0.31"
|
||||
rustyline = "14"
|
||||
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -3,7 +3,7 @@ extern crate dirs;
|
||||
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use deltachat::chat::{
|
||||
@@ -33,14 +33,6 @@ use tokio::fs;
|
||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||
async fn reset_tables(context: &Context, bits: i32) {
|
||||
println!("Resetting tables ({bits})...");
|
||||
if 0 != bits & 1 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM jobs;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(1) Jobs reset.");
|
||||
}
|
||||
if 0 != bits & 2 {
|
||||
context
|
||||
.sql()
|
||||
|
||||
@@ -9,7 +9,6 @@ extern crate deltachat;
|
||||
|
||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use ansi_term::Color;
|
||||
@@ -20,8 +19,7 @@ use deltachat::context::*;
|
||||
use deltachat::oauth2::*;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use deltachat::securejoin::*;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::{EventType, Events};
|
||||
use deltachat::EventType;
|
||||
use log::{error, info, warn};
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
use rustyline::error::ReadlineError;
|
||||
@@ -299,8 +297,8 @@ impl Highlighter for DcHelper {
|
||||
self.highlighter.highlight(line, pos)
|
||||
}
|
||||
|
||||
fn highlight_char(&self, line: &str, pos: usize) -> bool {
|
||||
self.highlighter.highlight_char(line, pos)
|
||||
fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
|
||||
self.highlighter.highlight_char(line, pos, forced)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +310,10 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
println!("Error: Bad arguments, expected [db-name].");
|
||||
bail!("No db-name specified");
|
||||
}
|
||||
let context = Context::new(Path::new(&args[1]), 0, Events::new(), StockStrings::new()).await?;
|
||||
let context = ContextBuilder::new(args[1].clone().into())
|
||||
.with_id(1)
|
||||
.open()
|
||||
.await?;
|
||||
|
||||
let events = context.get_event_emitter();
|
||||
tokio::task::spawn(async move {
|
||||
@@ -401,7 +402,7 @@ enum ExitResult {
|
||||
|
||||
async fn handle_cmd(
|
||||
line: &str,
|
||||
mut ctx: Context,
|
||||
ctx: Context,
|
||||
selected_chat: &mut ChatId,
|
||||
) -> Result<ExitResult, Error> {
|
||||
let mut args = line.splitn(2, ' ');
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
|
||||
requires = ["setuptools>=45"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.137.4"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -19,8 +20,9 @@ classifiers = [
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
dynamic = [
|
||||
"version"
|
||||
readme = "README.md"
|
||||
dependencies = [
|
||||
"imap-tools",
|
||||
]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
@@ -31,9 +33,6 @@ deltachat_rpc_client = [
|
||||
[project.entry-points.pytest11]
|
||||
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
root = ".."
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Delta Chat JSON-RPC high-level API"""
|
||||
|
||||
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
||||
from .account import Account
|
||||
from .chat import Chat
|
||||
|
||||
@@ -104,7 +104,11 @@ def _run_cli(
|
||||
if not client.is_configured():
|
||||
assert args.email, "Account is not configured and email must be provided"
|
||||
assert args.password, "Account is not configured and password must be provided"
|
||||
configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
|
||||
configure_thread = Thread(
|
||||
target=client.configure,
|
||||
daemon=True,
|
||||
kwargs={"email": args.email, "password": args.password},
|
||||
)
|
||||
configure_thread.start()
|
||||
client.run_forever()
|
||||
|
||||
@@ -168,3 +172,33 @@ def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
||||
return "removed", addr, addr
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class futuremethod: # noqa: N801
|
||||
"""Decorator for async methods."""
|
||||
|
||||
def __init__(self, func):
|
||||
self._func = func
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
def future(*args):
|
||||
generator = self._func(instance, *args)
|
||||
res = next(generator)
|
||||
|
||||
def f():
|
||||
try:
|
||||
generator.send(res())
|
||||
except StopIteration as e:
|
||||
return e.value
|
||||
|
||||
return f
|
||||
|
||||
def wrapper(*args):
|
||||
f = future(*args)
|
||||
return f()
|
||||
|
||||
wrapper.future = future
|
||||
return wrapper
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from warnings import warn
|
||||
|
||||
from ._utils import AttrDict
|
||||
from ._utils import AttrDict, futuremethod
|
||||
from .chat import Chat
|
||||
from .const import ChatlistFlag, ContactFlag, EventType, SpecialContactId
|
||||
from .contact import Contact
|
||||
@@ -28,6 +30,10 @@ class Account:
|
||||
"""Wait until the next event and return it."""
|
||||
return AttrDict(self._rpc.wait_for_event(self.id))
|
||||
|
||||
def clear_all_events(self):
|
||||
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||
self._rpc.clear_all_events(self.id)
|
||||
|
||||
def remove(self) -> None:
|
||||
"""Remove the account."""
|
||||
self._rpc.remove_account(self.id)
|
||||
@@ -76,9 +82,24 @@ class Account:
|
||||
"""Get self avatar."""
|
||||
return self.get_config("selfavatar")
|
||||
|
||||
def configure(self) -> None:
|
||||
def check_qr(self, qr):
|
||||
return self._rpc.check_qr(self.id, qr)
|
||||
|
||||
def set_config_from_qr(self, qr: str):
|
||||
self._rpc.set_config_from_qr(self.id, qr)
|
||||
|
||||
@futuremethod
|
||||
def configure(self):
|
||||
"""Configure an account."""
|
||||
self._rpc.configure(self.id)
|
||||
yield self._rpc.configure.future(self.id)
|
||||
|
||||
def bring_online(self):
|
||||
"""Start I/O and wait until IMAP becomes IDLE."""
|
||||
self.start_io()
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.IMAP_INBOX_IDLE:
|
||||
break
|
||||
|
||||
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||
"""Create a new Contact or return an existing one.
|
||||
@@ -97,6 +118,11 @@ class Account:
|
||||
obj = obj.get_snapshot().address
|
||||
return Contact(self, self._rpc.create_contact(self.id, obj, name))
|
||||
|
||||
def create_chat(self, account: "Account") -> Chat:
|
||||
addr = account.get_config("addr")
|
||||
contact = self.create_contact(addr)
|
||||
return contact.create_chat()
|
||||
|
||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||
"""Return Contact instance for the given contact ID."""
|
||||
return Contact(self, contact_id)
|
||||
@@ -106,7 +132,7 @@ class Account:
|
||||
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||
return contact_id and Contact(self, contact_id)
|
||||
|
||||
def get_blocked_contacts(self) -> List[AttrDict]:
|
||||
def get_blocked_contacts(self) -> list[AttrDict]:
|
||||
"""Return a list with snapshots of all blocked contacts."""
|
||||
contacts = self._rpc.get_blocked_contacts(self.id)
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
@@ -131,7 +157,7 @@ class Account:
|
||||
with_self: bool = False,
|
||||
verified_only: bool = False,
|
||||
snapshot: bool = False,
|
||||
) -> Union[List[Contact], List[AttrDict]]:
|
||||
) -> Union[list[Contact], list[AttrDict]]:
|
||||
"""Get a filtered list of contacts.
|
||||
|
||||
:param query: if a string is specified, only return contacts
|
||||
@@ -166,7 +192,7 @@ class Account:
|
||||
no_specials: bool = False,
|
||||
alldone_hint: bool = False,
|
||||
snapshot: bool = False,
|
||||
) -> Union[List[Chat], List[AttrDict]]:
|
||||
) -> Union[list[Chat], list[AttrDict]]:
|
||||
"""Return list of chats.
|
||||
|
||||
:param query: if a string is specified only chats matching this query are returned.
|
||||
@@ -218,13 +244,13 @@ class Account:
|
||||
The function returns immediately and the handshake runs in background, sending
|
||||
and receiving several messages.
|
||||
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||
See https://securejoin.readthedocs.io/en/latest/new.html for protocol details.
|
||||
See https://securejoin.delta.chat/ for protocol details.
|
||||
|
||||
:param qrdata: The text of the scanned QR code.
|
||||
"""
|
||||
return Chat(self, self._rpc.secure_join(self.id, qrdata))
|
||||
|
||||
def get_qr_code(self) -> Tuple[str, str]:
|
||||
def get_qr_code(self) -> tuple[str, str]:
|
||||
"""Get Setup-Contact QR Code text and SVG data.
|
||||
|
||||
this data needs to be transferred to another Delta Chat account
|
||||
@@ -236,15 +262,15 @@ class Account:
|
||||
"""Return the Message instance with the given ID."""
|
||||
return Message(self, msg_id)
|
||||
|
||||
def mark_seen_messages(self, messages: List[Message]) -> None:
|
||||
def mark_seen_messages(self, messages: list[Message]) -> None:
|
||||
"""Mark the given set of messages as seen."""
|
||||
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
||||
|
||||
def delete_messages(self, messages: List[Message]) -> None:
|
||||
def delete_messages(self, messages: list[Message]) -> None:
|
||||
"""Delete messages (local and remote)."""
|
||||
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
||||
|
||||
def get_fresh_messages(self) -> List[Message]:
|
||||
def get_fresh_messages(self) -> list[Message]:
|
||||
"""Return the list of fresh messages, newest messages first.
|
||||
|
||||
This call is intended for displaying notifications.
|
||||
@@ -254,12 +280,12 @@ class Account:
|
||||
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
def get_next_messages(self) -> List[Message]:
|
||||
def get_next_messages(self) -> list[Message]:
|
||||
"""Return a list of next messages."""
|
||||
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
def wait_next_messages(self) -> List[Message]:
|
||||
def wait_next_messages(self) -> list[Message]:
|
||||
"""Wait for new messages and return a list of them."""
|
||||
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
@@ -283,7 +309,13 @@ class Account:
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
||||
def wait_for_reactions_changed(self):
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.REACTIONS_CHANGED:
|
||||
return event
|
||||
|
||||
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||
warn(
|
||||
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .const import ChatVisibility, ViewType
|
||||
@@ -84,14 +86,16 @@ class Chat:
|
||||
self._rpc.set_chat_name(self.account.id, self.id, name)
|
||||
|
||||
def set_ephemeral_timer(self, timer: int) -> None:
|
||||
"""Set ephemeral timer of this chat."""
|
||||
"""Set ephemeral timer of this chat in seconds.
|
||||
|
||||
0 means the timer is disabled, use 1 for immediate deletion."""
|
||||
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||
|
||||
def get_encryption_info(self) -> str:
|
||||
"""Return encryption info for this chat."""
|
||||
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
||||
|
||||
def get_qr_code(self) -> Tuple[str, str]:
|
||||
def get_qr_code(self) -> tuple[str, str]:
|
||||
"""Get Join-Group QR code text and SVG data."""
|
||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
||||
|
||||
@@ -115,7 +119,7 @@ class Chat:
|
||||
html: Optional[str] = None,
|
||||
viewtype: Optional[ViewType] = None,
|
||||
file: Optional[str] = None,
|
||||
location: Optional[Tuple[float, float]] = None,
|
||||
location: Optional[tuple[float, float]] = None,
|
||||
override_sender_name: Optional[str] = None,
|
||||
quoted_msg: Optional[Union[int, Message]] = None,
|
||||
) -> Message:
|
||||
@@ -140,6 +144,10 @@ class Chat:
|
||||
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def send_file(self, path):
|
||||
"""Send a file and return the resulting Message instance."""
|
||||
return self.send_message(file=path)
|
||||
|
||||
def send_videochat_invitation(self) -> Message:
|
||||
"""Send a videochat invitation and return the resulting Message instance."""
|
||||
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
|
||||
@@ -150,7 +158,7 @@ class Chat:
|
||||
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def forward_messages(self, messages: List[Message]) -> None:
|
||||
def forward_messages(self, messages: list[Message]) -> None:
|
||||
"""Forward a list of messages to this chat."""
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
||||
@@ -182,7 +190,7 @@ class Chat:
|
||||
snapshot["message"] = Message(self.account, snapshot.id)
|
||||
return snapshot
|
||||
|
||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
|
||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
|
||||
"""get the list of messages in this chat."""
|
||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||
@@ -217,7 +225,7 @@ class Chat:
|
||||
contact_id = cnt
|
||||
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
|
||||
|
||||
def get_contacts(self) -> List[Contact]:
|
||||
def get_contacts(self) -> list[Contact]:
|
||||
"""Get the contacts belonging to this chat.
|
||||
|
||||
For single/direct chats self-address is not included.
|
||||
@@ -241,7 +249,7 @@ class Chat:
|
||||
contact: Optional[Contact] = None,
|
||||
timestamp_from: Optional["datetime"] = None,
|
||||
timestamp_to: Optional["datetime"] = None,
|
||||
) -> List[AttrDict]:
|
||||
) -> list[AttrDict]:
|
||||
"""Get list of location snapshots for the given contact in the given timespan."""
|
||||
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
|
||||
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
||||
@@ -249,7 +257,7 @@ class Chat:
|
||||
|
||||
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
||||
locations = []
|
||||
contacts: Dict[int, Contact] = {}
|
||||
contacts: dict[int, Contact] = {}
|
||||
for loc in result:
|
||||
location = AttrDict(loc)
|
||||
location["chat"] = self
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""Event loop implementations offering high level event handling/hooking."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
@@ -38,16 +38,16 @@ class Client:
|
||||
def __init__(
|
||||
self,
|
||||
account: "Account",
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, EventFilter]]]] = None,
|
||||
hooks: Optional[Iterable[tuple[Callable, Union[type, EventFilter]]]] = None,
|
||||
logger: Optional[logging.Logger] = None,
|
||||
) -> None:
|
||||
self.account = account
|
||||
self.logger = logger or logging
|
||||
self._hooks: Dict[type, Set[tuple]] = {}
|
||||
self._hooks: dict[type, set[tuple]] = {}
|
||||
self._should_process_messages = 0
|
||||
self.add_hooks(hooks or [])
|
||||
|
||||
def add_hooks(self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]) -> None:
|
||||
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
|
||||
for hook, event in hooks:
|
||||
self.add_hook(hook, event)
|
||||
|
||||
|
||||
@@ -59,6 +59,17 @@ class EventType(str, Enum):
|
||||
SELFAVATAR_CHANGED = "SelfavatarChanged"
|
||||
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
|
||||
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
||||
CHATLIST_CHANGED = "ChatlistChanged"
|
||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||
|
||||
|
||||
class ChatId(IntEnum):
|
||||
"""Special chat ids"""
|
||||
|
||||
TRASH = 3
|
||||
ARCHIVED_LINK = 6
|
||||
ALLDONE_HINT = 7
|
||||
LAST_SPECIAL = 9
|
||||
|
||||
|
||||
class ChatType(IntEnum):
|
||||
@@ -122,3 +133,107 @@ class SystemMessageType(str, Enum):
|
||||
EPHEMERAL_TIMER_CHANGED = "EphemeralTimerChanged"
|
||||
MULTI_DEVICE_SYNC = "MultiDeviceSync"
|
||||
WEBXDC_INFO_MESSAGE = "WebxdcInfoMessage"
|
||||
|
||||
|
||||
class MessageState(IntEnum):
|
||||
"""State of the message."""
|
||||
|
||||
UNDEFINED = 0
|
||||
IN_FRESH = 10
|
||||
IN_NOTICED = 13
|
||||
IN_SEEN = 16
|
||||
OUT_PREPARING = 18
|
||||
OUT_DRAFT = 19
|
||||
OUT_PENDING = 20
|
||||
OUT_FAILED = 24
|
||||
OUT_DELIVERED = 26
|
||||
OUT_MDN_RCVD = 28
|
||||
|
||||
|
||||
class MessageId(IntEnum):
|
||||
"""Special message ids"""
|
||||
|
||||
DAYMARKER = 9
|
||||
LAST_SPECIAL = 9
|
||||
|
||||
|
||||
class CertificateChecks(IntEnum):
|
||||
"""Certificate checks mode"""
|
||||
|
||||
AUTOMATIC = 0
|
||||
STRICT = 1
|
||||
ACCEPT_INVALID_CERTIFICATES = 3
|
||||
|
||||
|
||||
class Connectivity(IntEnum):
|
||||
"""Connectivity states"""
|
||||
|
||||
NOT_CONNECTED = 1000
|
||||
CONNECTING = 2000
|
||||
WORKING = 3000
|
||||
CONNECTED = 4000
|
||||
|
||||
|
||||
class KeyGenType(IntEnum):
|
||||
"""Type of the key to generate"""
|
||||
|
||||
DEFAULT = 0
|
||||
RSA2048 = 1
|
||||
ED25519 = 2
|
||||
RSA4096 = 3
|
||||
|
||||
|
||||
# "Lp" means "login parameters"
|
||||
class LpAuthFlag(IntEnum):
|
||||
"""Authorization flags"""
|
||||
|
||||
OAUTH2 = 0x2
|
||||
NORMAL = 0x4
|
||||
|
||||
|
||||
class MediaQuality(IntEnum):
|
||||
"""Media quality setting"""
|
||||
|
||||
BALANCED = 0
|
||||
WORSE = 1
|
||||
|
||||
|
||||
class ProviderStatus(IntEnum):
|
||||
"""Provider status according to manual testing"""
|
||||
|
||||
OK = 1
|
||||
PREPARATION = 2
|
||||
BROKEN = 3
|
||||
|
||||
|
||||
class PushNotifyState(IntEnum):
|
||||
"""Push notifications state"""
|
||||
|
||||
NOT_CONNECTED = 0
|
||||
HEARTBEAT = 1
|
||||
CONNECTED = 2
|
||||
|
||||
|
||||
class ShowEmails(IntEnum):
|
||||
"""Show emails mode"""
|
||||
|
||||
OFF = 0
|
||||
ACCEPTED_CONTACTS = 1
|
||||
ALL = 2
|
||||
|
||||
|
||||
class SocketSecurity(IntEnum):
|
||||
"""Socket security"""
|
||||
|
||||
AUTOMATIC = 0
|
||||
SSL = 1
|
||||
STARTTLS = 2
|
||||
PLAIN = 3
|
||||
|
||||
|
||||
class VideochatType(IntEnum):
|
||||
"""Video chat URL type"""
|
||||
|
||||
UNKNOWN = 0
|
||||
BASICWEBRTC = 1
|
||||
JITSI = 2
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .account import Account
|
||||
@@ -21,7 +23,7 @@ class DeltaChat:
|
||||
account_id = self.rpc.add_account()
|
||||
return Account(self, account_id)
|
||||
|
||||
def get_all_accounts(self) -> List[Account]:
|
||||
def get_all_accounts(self) -> list[Account]:
|
||||
"""Return a list of all available accounts."""
|
||||
account_ids = self.rpc.get_all_account_ids()
|
||||
return [Account(self, account_id) for account_id in account_ids]
|
||||
@@ -44,6 +46,6 @@ class DeltaChat:
|
||||
"""Get information about the Delta Chat core in this system."""
|
||||
return AttrDict(self.rpc.get_system_info())
|
||||
|
||||
def set_translations(self, translations: Dict[str, str]) -> None:
|
||||
def set_translations(self, translations: dict[str, str]) -> None:
|
||||
"""Set stock translation strings."""
|
||||
self.rpc.set_stock_strings(translations)
|
||||
|
||||
226
deltachat-rpc-client/src/deltachat_rpc_client/direct_imap.py
Normal file
226
deltachat-rpc-client/src/deltachat_rpc_client/direct_imap.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
Internal Python-level IMAP handling used by the tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import imaplib
|
||||
import io
|
||||
import pathlib
|
||||
import ssl
|
||||
from contextlib import contextmanager
|
||||
|
||||
from imap_tools import (
|
||||
AND,
|
||||
Header,
|
||||
MailBox,
|
||||
MailBoxTls,
|
||||
MailMessage,
|
||||
MailMessageFlags,
|
||||
errors,
|
||||
)
|
||||
|
||||
from . import Account, const
|
||||
|
||||
FLAGS = b"FLAGS"
|
||||
FETCH = b"FETCH"
|
||||
ALL = "1:*"
|
||||
|
||||
|
||||
class DirectImap:
|
||||
def __init__(self, account: Account) -> None:
|
||||
self.account = account
|
||||
self.logid = account.get_config("displayname") or id(account)
|
||||
self._idling = False
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
host = self.account.get_config("configured_mail_server")
|
||||
port = int(self.account.get_config("configured_mail_port"))
|
||||
security = int(self.account.get_config("configured_mail_security"))
|
||||
|
||||
user = self.account.get_config("addr")
|
||||
pw = self.account.get_config("mail_pw")
|
||||
|
||||
if security == const.SocketSecurity.PLAIN:
|
||||
ssl_context = None
|
||||
else:
|
||||
ssl_context = ssl.create_default_context()
|
||||
|
||||
# don't check if certificate hostname doesn't match target hostname
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
# don't check if the certificate is trusted by a certificate authority
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
if security == const.SocketSecurity.STARTTLS:
|
||||
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
|
||||
elif security == const.SocketSecurity.PLAIN or security == const.SocketSecurity.SSL:
|
||||
self.conn = MailBox(host, port, ssl_context=ssl_context)
|
||||
self.conn.login(user, pw)
|
||||
|
||||
self.select_folder("INBOX")
|
||||
|
||||
def shutdown(self):
|
||||
try:
|
||||
self.conn.logout()
|
||||
except (OSError, imaplib.IMAP4.abort):
|
||||
print("Could not logout direct_imap conn")
|
||||
|
||||
def create_folder(self, foldername):
|
||||
try:
|
||||
self.conn.folder.create(foldername)
|
||||
except errors.MailboxFolderCreateError as e:
|
||||
print("Can't create", foldername, "probably it already exists:", str(e))
|
||||
|
||||
def select_folder(self, foldername: str) -> tuple:
|
||||
assert not self._idling
|
||||
return self.conn.folder.set(foldername)
|
||||
|
||||
def select_config_folder(self, config_name: str):
|
||||
"""Return info about selected folder if it is
|
||||
configured, otherwise None.
|
||||
"""
|
||||
if "_" not in config_name:
|
||||
config_name = f"configured_{config_name}_folder"
|
||||
foldername = self.account.get_config(config_name)
|
||||
if foldername:
|
||||
return self.select_folder(foldername)
|
||||
return None
|
||||
|
||||
def list_folders(self) -> list[str]:
|
||||
"""return list of all existing folder names."""
|
||||
assert not self._idling
|
||||
return [folder.name for folder in self.conn.folder.list()]
|
||||
|
||||
def delete(self, uid_list: str, expunge=True):
|
||||
"""delete a range of messages (imap-syntax).
|
||||
If expunge is true, perform the expunge-operation
|
||||
to make sure the messages are really gone and not
|
||||
just flagged as deleted.
|
||||
"""
|
||||
self.conn.client.uid("STORE", uid_list, "+FLAGS", r"(\Deleted)")
|
||||
if expunge:
|
||||
self.conn.expunge()
|
||||
|
||||
def get_all_messages(self) -> list[MailMessage]:
|
||||
assert not self._idling
|
||||
return list(self.conn.fetch())
|
||||
|
||||
def get_unread_messages(self) -> list[str]:
|
||||
assert not self._idling
|
||||
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
|
||||
|
||||
def mark_all_read(self):
|
||||
messages = self.get_unread_messages()
|
||||
if messages:
|
||||
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
|
||||
print("marked seen:", messages, res)
|
||||
|
||||
def get_unread_cnt(self) -> int:
|
||||
return len(self.get_unread_messages())
|
||||
|
||||
def dump_imap_structures(self, dir, logfile):
|
||||
assert not self._idling
|
||||
stream = io.StringIO()
|
||||
|
||||
def log(*args, **kwargs):
|
||||
kwargs["file"] = stream
|
||||
print(*args, **kwargs)
|
||||
|
||||
empty_folders = []
|
||||
for imapfolder in self.list_folders():
|
||||
self.select_folder(imapfolder)
|
||||
messages = list(self.get_all_messages())
|
||||
if not messages:
|
||||
empty_folders.append(imapfolder)
|
||||
continue
|
||||
|
||||
log("---------", imapfolder, len(messages), "messages ---------")
|
||||
# get message content without auto-marking it as seen
|
||||
# fetching 'RFC822' would mark it as seen.
|
||||
for msg in self.conn.fetch(mark_seen=False):
|
||||
body = getattr(msg.obj, "text", None)
|
||||
if not body:
|
||||
body = getattr(msg.obj, "html", None)
|
||||
if not body:
|
||||
log("Message", msg.uid, "has empty body")
|
||||
continue
|
||||
|
||||
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
fn = path.joinpath(str(msg.uid))
|
||||
fn.write_bytes(body)
|
||||
log("Message", msg.uid, fn)
|
||||
log(
|
||||
"Message",
|
||||
msg.uid,
|
||||
msg.flags,
|
||||
"Message-Id:",
|
||||
msg.obj.get("Message-Id"),
|
||||
)
|
||||
|
||||
if empty_folders:
|
||||
log("--------- EMPTY FOLDERS:", empty_folders)
|
||||
|
||||
print(stream.getvalue(), file=logfile)
|
||||
|
||||
@contextmanager
|
||||
def idle(self):
|
||||
"""return Idle ContextManager."""
|
||||
idle_manager = IdleManager(self)
|
||||
try:
|
||||
yield idle_manager
|
||||
finally:
|
||||
idle_manager.done()
|
||||
|
||||
def append(self, folder: str, msg: str):
|
||||
"""Upload a message to *folder*.
|
||||
Trailing whitespace or a linebreak at the beginning will be removed automatically.
|
||||
"""
|
||||
if msg.startswith("\n"):
|
||||
msg = msg[1:]
|
||||
msg = "\n".join([s.lstrip() for s in msg.splitlines()])
|
||||
self.conn.append(bytes(msg, encoding="ascii"), folder)
|
||||
|
||||
def get_uid_by_message_id(self, message_id) -> str:
|
||||
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
|
||||
if len(msgs) == 0:
|
||||
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||
return msgs[0]
|
||||
|
||||
|
||||
class IdleManager:
|
||||
def __init__(self, direct_imap) -> None:
|
||||
self.direct_imap = direct_imap
|
||||
self.log = direct_imap.account.log
|
||||
# fetch latest messages before starting idle so that it only
|
||||
# returns messages that arrive anew
|
||||
self.direct_imap.conn.fetch("1:*")
|
||||
self.direct_imap.conn.idle.start()
|
||||
|
||||
def check(self, timeout=None) -> list[bytes]:
|
||||
"""(blocking) wait for next idle message from server."""
|
||||
self.log("imap-direct: calling idle_check")
|
||||
res = self.direct_imap.conn.idle.poll(timeout=timeout)
|
||||
self.log(f"imap-direct: idle_check returned {res!r}")
|
||||
return res
|
||||
|
||||
def wait_for_new_message(self, timeout=None) -> bytes:
|
||||
while True:
|
||||
for item in self.check(timeout=timeout):
|
||||
if b"EXISTS" in item or b"RECENT" in item:
|
||||
return item
|
||||
|
||||
def wait_for_seen(self, timeout=None) -> int:
|
||||
"""Return first message with SEEN flag from a running idle-stream."""
|
||||
while True:
|
||||
for item in self.check(timeout=timeout):
|
||||
if FETCH in item:
|
||||
self.log(str(item))
|
||||
if FLAGS in item and rb"\Seen" in item:
|
||||
return int(item.split(b" ")[1])
|
||||
|
||||
def done(self):
|
||||
"""send idle-done to server if we are currently in idle mode."""
|
||||
return self.direct_imap.conn.idle.stop()
|
||||
@@ -1,7 +1,10 @@
|
||||
"""High-level classes for event processing and filtering."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Union
|
||||
|
||||
from .const import EventType
|
||||
|
||||
@@ -262,9 +265,9 @@ class HookCollection:
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._hooks: Set[Tuple[Callable, Union[type, EventFilter]]] = set()
|
||||
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[Callable, Union[type, EventFilter]]]:
|
||||
def __iter__(self) -> Iterator[tuple[Callable, Union[type, EventFilter]]]:
|
||||
return iter(self._hooks)
|
||||
|
||||
def on(self, event: Union[type, EventFilter]) -> Callable: # noqa
|
||||
|
||||
@@ -3,6 +3,7 @@ from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .const import EventType
|
||||
from .contact import Contact
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -21,9 +22,10 @@ class Message:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.account._rpc
|
||||
|
||||
def send_reaction(self, *reaction: str):
|
||||
def send_reaction(self, *reaction: str) -> "Message":
|
||||
"""Send a reaction to this message."""
|
||||
self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||
msg_id = self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def get_snapshot(self) -> AttrDict:
|
||||
"""Get a snapshot with the properties of this message."""
|
||||
@@ -61,3 +63,10 @@ class Message:
|
||||
|
||||
def get_webxdc_info(self) -> dict:
|
||||
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||
|
||||
def wait_until_delivered(self) -> None:
|
||||
"""Consume events until the message is delivered."""
|
||||
while True:
|
||||
event = self.account.wait_for_event()
|
||||
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
|
||||
break
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
from typing import AsyncGenerator, List, Optional
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
|
||||
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
|
||||
from ._utils import futuremethod
|
||||
from .rpc import Rpc
|
||||
|
||||
|
||||
@@ -37,9 +40,10 @@ class ACFactory:
|
||||
assert not account.is_configured()
|
||||
return account
|
||||
|
||||
def new_configured_account(self) -> Account:
|
||||
@futuremethod
|
||||
def new_configured_account(self):
|
||||
account = self.new_preconfigured_account()
|
||||
account.configure()
|
||||
yield account.configure.future()
|
||||
assert account.is_configured()
|
||||
return account
|
||||
|
||||
@@ -49,17 +53,15 @@ class ACFactory:
|
||||
bot.configure(credentials["email"], credentials["password"])
|
||||
return bot
|
||||
|
||||
def get_online_account(self) -> Account:
|
||||
account = self.new_configured_account()
|
||||
account.start_io()
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.IMAP_INBOX_IDLE:
|
||||
break
|
||||
@futuremethod
|
||||
def get_online_account(self):
|
||||
account = yield self.new_configured_account.future()
|
||||
account.bring_online()
|
||||
return account
|
||||
|
||||
def get_online_accounts(self, num: int) -> List[Account]:
|
||||
return [self.get_online_account() for _ in range(num)]
|
||||
def get_online_accounts(self, num: int) -> list[Account]:
|
||||
futures = [self.get_online_account.future() for _ in range(num)]
|
||||
return [f() for f in futures]
|
||||
|
||||
def resetup_account(self, ac: Account) -> Account:
|
||||
"""Resetup account from scratch, losing the encryption key."""
|
||||
@@ -71,6 +73,10 @@ class ACFactory:
|
||||
ac_clone.configure()
|
||||
return ac_clone
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
def send_message(
|
||||
self,
|
||||
to_account: Account,
|
||||
|
||||
@@ -1,18 +1,62 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from queue import Queue
|
||||
from queue import Empty, Queue
|
||||
from threading import Event, Thread
|
||||
from typing import Any, Dict, Iterator, Optional
|
||||
from typing import Any, Iterator, Optional
|
||||
|
||||
|
||||
class JsonRpcError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RpcFuture:
|
||||
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
|
||||
self.rpc = rpc
|
||||
self.request_id = request_id
|
||||
self.event = event
|
||||
|
||||
def __call__(self):
|
||||
self.event.wait()
|
||||
response = self.rpc.request_results.pop(self.request_id)
|
||||
if "error" in response:
|
||||
raise JsonRpcError(response["error"])
|
||||
if "result" in response:
|
||||
return response["result"]
|
||||
return None
|
||||
|
||||
|
||||
class RpcMethod:
|
||||
def __init__(self, rpc: "Rpc", name: str):
|
||||
self.rpc = rpc
|
||||
self.name = name
|
||||
|
||||
def __call__(self, *args) -> Any:
|
||||
"""Synchronously calls JSON-RPC method."""
|
||||
future = self.future(*args)
|
||||
return future()
|
||||
|
||||
def future(self, *args) -> Any:
|
||||
"""Asynchronously calls JSON-RPC method."""
|
||||
request_id = next(self.rpc.id_iterator)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": self.name,
|
||||
"params": args,
|
||||
"id": request_id,
|
||||
}
|
||||
event = Event()
|
||||
self.rpc.request_events[request_id] = event
|
||||
self.rpc.request_queue.put(request)
|
||||
|
||||
return RpcFuture(self.rpc, request_id, event)
|
||||
|
||||
|
||||
class Rpc:
|
||||
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
||||
"""The given arguments will be passed to subprocess.Popen()"""
|
||||
@@ -25,11 +69,11 @@ class Rpc:
|
||||
self._kwargs = kwargs
|
||||
self.process: subprocess.Popen
|
||||
self.id_iterator: Iterator[int]
|
||||
self.event_queues: Dict[int, Queue]
|
||||
self.event_queues: dict[int, Queue]
|
||||
# Map from request ID to `threading.Event`.
|
||||
self.request_events: Dict[int, Event]
|
||||
self.request_events: dict[int, Event]
|
||||
# Map from request ID to the result.
|
||||
self.request_results: Dict[int, Any]
|
||||
self.request_results: dict[int, Any]
|
||||
self.request_queue: Queue[Any]
|
||||
self.closing: bool
|
||||
self.reader_thread: Thread
|
||||
@@ -144,25 +188,14 @@ class Rpc:
|
||||
queue = self.get_queue(account_id)
|
||||
return queue.get()
|
||||
|
||||
def clear_all_events(self, account_id: int):
|
||||
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||
queue = self.get_queue(account_id)
|
||||
try:
|
||||
while True:
|
||||
queue.get_nowait()
|
||||
except Empty:
|
||||
pass
|
||||
|
||||
def __getattr__(self, attr: str):
|
||||
def method(*args) -> Any:
|
||||
request_id = next(self.id_iterator)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": attr,
|
||||
"params": args,
|
||||
"id": request_id,
|
||||
}
|
||||
event = Event()
|
||||
self.request_events[request_id] = event
|
||||
self.request_queue.put(request)
|
||||
event.wait()
|
||||
|
||||
response = self.request_results.pop(request_id)
|
||||
if "error" in response:
|
||||
raise JsonRpcError(response["error"])
|
||||
if "result" in response:
|
||||
return response["result"]
|
||||
return None
|
||||
|
||||
return method
|
||||
return RpcMethod(self, attr)
|
||||
|
||||
218
deltachat-rpc-client/tests/test_chatlist_events.py
Normal file
218
deltachat-rpc-client/tests/test_chatlist_events.py
Normal file
@@ -0,0 +1,218 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from deltachat_rpc_client import Account, EventType, const
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||
|
||||
|
||||
def wait_for_chatlist_and_specific_item(account, chat_id):
|
||||
first_event = ""
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.CHATLIST_CHANGED:
|
||||
first_event = "change"
|
||||
break
|
||||
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
|
||||
first_event = "item_change"
|
||||
break
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.CHATLIST_CHANGED and first_event == "item_change":
|
||||
break
|
||||
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id and first_event == "change":
|
||||
break
|
||||
|
||||
|
||||
def wait_for_chatlist_specific_item(account, chat_id):
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
|
||||
break
|
||||
|
||||
|
||||
def wait_for_chatlist(account):
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.CHATLIST_CHANGED:
|
||||
break
|
||||
|
||||
|
||||
def test_delivery_status(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test change status on chatlistitem when status changes (delivered, read)
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
|
||||
alice.clear_all_events()
|
||||
bob.stop_io()
|
||||
alice.stop_io()
|
||||
alice_chat_bob.send_text("hi")
|
||||
wait_for_chatlist_and_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||
|
||||
alice.clear_all_events()
|
||||
alice.start_io()
|
||||
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||
|
||||
bob.clear_all_events()
|
||||
bob.start_io()
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
msg.get_snapshot().chat.accept()
|
||||
msg.mark_seen()
|
||||
|
||||
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
|
||||
assert chat_item["summaryStatus"] == const.MessageState.OUT_DELIVERED
|
||||
|
||||
alice.clear_all_events()
|
||||
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event.kind == EventType.MSG_READ:
|
||||
break
|
||||
|
||||
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
|
||||
assert chat_item["summaryStatus"] == const.MessageState.OUT_MDN_RCVD
|
||||
|
||||
|
||||
def test_delivery_status_failed(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test change status on chatlistitem when status changes failed
|
||||
"""
|
||||
(alice,) = acfactory.get_online_accounts(1)
|
||||
|
||||
invalid_contact = alice.create_contact("example@example.com", "invalid address")
|
||||
invalid_chat = alice.get_chat_by_id(alice._rpc.create_chat_by_contact_id(alice.id, invalid_contact.id))
|
||||
|
||||
alice.clear_all_events()
|
||||
|
||||
failing_message = invalid_chat.send_text("test")
|
||||
|
||||
wait_for_chatlist_and_specific_item(alice, invalid_chat.id)
|
||||
|
||||
assert failing_message.get_snapshot().state == const.MessageState.OUT_PENDING
|
||||
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event.kind == EventType.MSG_FAILED:
|
||||
break
|
||||
|
||||
wait_for_chatlist_specific_item(alice, invalid_chat.id)
|
||||
|
||||
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
|
||||
|
||||
|
||||
def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test if download on demand emits chatlist update events.
|
||||
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("hi")
|
||||
|
||||
alice.set_config("download_limit", "1")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
chat_id = msg.get_snapshot().chat_id
|
||||
msg.get_snapshot().chat.accept()
|
||||
bob.get_chat_by_id(chat_id).send_message(
|
||||
"Hello World, this message is bigger than 5 bytes",
|
||||
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
|
||||
)
|
||||
|
||||
msg_id = alice.wait_for_incoming_msg_event().msg_id
|
||||
|
||||
assert alice.get_message_by_id(msg_id).get_snapshot().download_state == const.DownloadState.AVAILABLE
|
||||
|
||||
alice.clear_all_events()
|
||||
chat_id = alice.get_message_by_id(msg_id).get_snapshot().chat_id
|
||||
alice._rpc.download_full_message(alice.id, msg_id)
|
||||
|
||||
wait_for_chatlist_specific_item(alice, chat_id)
|
||||
|
||||
|
||||
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("hi")
|
||||
|
||||
bob.wait_for_incoming_msg_event()
|
||||
|
||||
alice_second_device: Account = acfactory.get_unconfigured_account()
|
||||
|
||||
alice._rpc.provide_backup.future(alice.id)
|
||||
backup_code = alice._rpc.get_backup_qr(alice.id)
|
||||
alice_second_device._rpc.get_backup(alice_second_device.id, backup_code)
|
||||
alice_second_device.start_io()
|
||||
alice.clear_all_events()
|
||||
alice_second_device.clear_all_events()
|
||||
bob.clear_all_events()
|
||||
return [alice, alice_second_device, bob, alice_chat_bob]
|
||||
|
||||
|
||||
def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test that chatlist changed events are emitted for the second device
|
||||
when the message is marked as read on the first device
|
||||
"""
|
||||
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||
|
||||
alice_chat_bob.send_text("hello")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
bob_chat_id = msg.get_snapshot().chat_id
|
||||
msg.get_snapshot().chat.accept()
|
||||
|
||||
alice.clear_all_events()
|
||||
alice_second_device.clear_all_events()
|
||||
bob.get_chat_by_id(bob_chat_id).send_text("hello")
|
||||
|
||||
# make sure alice_second_device already received the message
|
||||
alice_second_device.wait_for_incoming_msg_event()
|
||||
|
||||
event = alice.wait_for_incoming_msg_event()
|
||||
msg = alice.get_message_by_id(event.msg_id)
|
||||
alice_second_device.clear_all_events()
|
||||
msg.mark_seen()
|
||||
|
||||
wait_for_chatlist_specific_item(bob, bob_chat_id)
|
||||
wait_for_chatlist_specific_item(alice, alice_chat_bob.id)
|
||||
|
||||
|
||||
def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test multidevice sync: syncing chat visibility and muting across multiple devices
|
||||
"""
|
||||
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||
|
||||
alice_chat_bob.archive()
|
||||
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().archived
|
||||
|
||||
alice_second_device.clear_all_events()
|
||||
alice_chat_bob.pin()
|
||||
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||
|
||||
alice_second_device.clear_all_events()
|
||||
alice_chat_bob.mute()
|
||||
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().is_muted
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from deltachat_rpc_client import Chat, SpecialContactId
|
||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||
|
||||
|
||||
def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||
@@ -164,6 +164,21 @@ def test_qr_readreceipt(acfactory) -> None:
|
||||
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||
|
||||
|
||||
def test_setup_contact_resetup(acfactory) -> None:
|
||||
"""Tests that setup contact works after Alice resets the device and changes the key."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
alice = acfactory.resetup_account(alice)
|
||||
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
def test_verified_group_recovery(acfactory) -> None:
|
||||
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
@@ -175,7 +190,7 @@ def test_verified_group_recovery(acfactory) -> None:
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
# ac1 has ac2 directly verified.
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
@@ -183,7 +198,8 @@ def test_verified_group_recovery(acfactory) -> None:
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
ac3_chat = ac3.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
ac3.wait_for_securejoin_joiner_success()
|
||||
ac3.wait_for_incoming_msg_event() # Member added
|
||||
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
@@ -191,8 +207,7 @@ def test_verified_group_recovery(acfactory) -> None:
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
qr_code, _svg = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
|
||||
ac3.wait_for_securejoin_inviter_success()
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
logging.info("ac3 sends a message to the group")
|
||||
assert len(ac3_chat.get_contacts()) == 3
|
||||
@@ -239,7 +254,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
# ac1 has ac2 directly verified.
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
@@ -247,7 +262,8 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
ac3_chat = ac3.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
ac3.wait_for_securejoin_joiner_success()
|
||||
ac3.wait_for_incoming_msg_event() # Member added
|
||||
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
@@ -255,8 +271,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
qr_code, _svg = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
|
||||
ac3.wait_for_securejoin_inviter_success()
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
logging.info("ac3 sends a message to the group")
|
||||
assert len(ac3_chat.get_contacts()) == 3
|
||||
@@ -564,3 +579,40 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
|
||||
# ac1 is still "not verified" for ac2 due to inconsistent state.
|
||||
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||
|
||||
|
||||
def test_withdraw_securejoin_qr(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("Bob joins verified group")
|
||||
|
||||
qr_code, _svg = alice_chat.get_qr_code()
|
||||
bob_chat = bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
|
||||
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||
bob_chat.leave()
|
||||
|
||||
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
|
||||
|
||||
logging.info("Alice withdraws QR code.")
|
||||
qr = alice.check_qr(qr_code)
|
||||
assert qr["kind"] == "withdrawVerifyGroup"
|
||||
alice.set_config_from_qr(qr_code)
|
||||
|
||||
logging.info("Bob scans withdrawn QR code.")
|
||||
bob_chat = bob.secure_join(qr_code)
|
||||
|
||||
logging.info("Bob scanned withdrawn QR code")
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event.kind == EventType.MSGS_CHANGED and event.chat_id != 0:
|
||||
break
|
||||
snapshot = alice.get_message_by_id(event.msg_id).get_snapshot()
|
||||
assert snapshot.text == "Cannot establish guaranteed end-to-end encryption with {}".format(bob.get_config("addr"))
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import concurrent.futures
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from deltachat_rpc_client import EventType, events
|
||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||
from deltachat_rpc_client.const import DownloadState, MessageState
|
||||
from deltachat_rpc_client.direct_imap import DirectImap
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -439,3 +444,172 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||
message = alice.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.show_padlock
|
||||
|
||||
|
||||
def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
|
||||
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
|
||||
messages are received out of order".
|
||||
|
||||
If the Inbox contains X small messages followed by Y large messages followed by Z small
|
||||
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
|
||||
|
||||
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
|
||||
with online test as follows:
|
||||
- Bob enables download limit and goes offline.
|
||||
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
|
||||
- Bob goes online
|
||||
- Bob first processes a reaction message and throws it away because there is no corresponding
|
||||
message, then processes a partially downloaded message.
|
||||
- As a result, Bob does not see a reaction
|
||||
"""
|
||||
download_limit = 300000
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_addr = ac1.get_config("addr")
|
||||
chat = ac1.create_chat(ac2)
|
||||
ac2.set_config("download_limit", str(download_limit))
|
||||
ac2.stop_io()
|
||||
|
||||
logging.info("sending small+large messages from ac1 to ac2")
|
||||
msgs = []
|
||||
msgs.append(chat.send_text("hi"))
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
msgs.append(chat.send_file(str(path)))
|
||||
for m in msgs:
|
||||
m.wait_until_delivered()
|
||||
|
||||
logging.info("sending a reaction to the large message from ac1 to ac2")
|
||||
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
|
||||
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
|
||||
# have a later INTERNALDATE.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msgs.append(msgs[-1].send_reaction(react_str))
|
||||
msgs[-1].wait_until_delivered()
|
||||
|
||||
ac2.start_io()
|
||||
|
||||
logging.info("wait for ac2 to receive a reaction")
|
||||
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||
assert msg2.get_sender_contact().get_snapshot().address == ac1_addr
|
||||
assert msg2.get_snapshot().download_state == DownloadState.AVAILABLE
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].get_snapshot().address == ac1_addr
|
||||
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||
|
||||
|
||||
def test_reactions_for_a_reordering_move(acfactory):
|
||||
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
|
||||
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
|
||||
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
|
||||
messages they refer to and thus dropped.
|
||||
"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
ac2 = acfactory.new_preconfigured_account()
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
ac2.configure()
|
||||
ac2.bring_online()
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac2.stop_io()
|
||||
|
||||
logging.info("sending message + reaction from ac1 to ac2")
|
||||
msg1 = chat1.send_text("hi")
|
||||
msg1.wait_until_delivered()
|
||||
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
|
||||
# order by DC, and most (if not all) mail servers provide only seconds precision.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msg1.send_reaction(react_str).wait_until_delivered()
|
||||
|
||||
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||
ac2_direct_imap = DirectImap(ac2)
|
||||
ac2_direct_imap.connect()
|
||||
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
|
||||
ac2_direct_imap.conn.move(uid, "DeltaChat")
|
||||
|
||||
logging.info("receiving messages by ac2")
|
||||
ac2.start_io()
|
||||
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||
assert msg2.get_snapshot().text == msg1.get_snapshot().text
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].get_snapshot().address == ac1.get_config("addr")
|
||||
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n_accounts", [3, 2])
|
||||
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
download_limit = 300000
|
||||
|
||||
alice, *others = acfactory.get_online_accounts(n_accounts)
|
||||
bob = others[0]
|
||||
|
||||
alice_group = alice.create_group("test group")
|
||||
for account in others:
|
||||
chat = account.create_chat(alice)
|
||||
chat.send_text("Hello Alice!")
|
||||
assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!"
|
||||
|
||||
contact_addr = account.get_config("addr")
|
||||
contact = alice.create_contact(contact_addr, "")
|
||||
|
||||
alice_group.add_contact(contact)
|
||||
|
||||
if n_accounts == 2:
|
||||
bob_chat_alice = bob.create_chat(alice)
|
||||
bob.set_config("download_limit", str(download_limit))
|
||||
|
||||
alice_group.send_text("hi")
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "hi"
|
||||
bob_group = snapshot.chat
|
||||
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
|
||||
for i in range(10):
|
||||
logging.info("Sending message %s", i)
|
||||
alice_group.send_file(str(path))
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||
if n_accounts > 2:
|
||||
assert snapshot.chat == bob_group
|
||||
else:
|
||||
# Group contains only Alice and Bob,
|
||||
# so partially downloaded messages are
|
||||
# hard to distinguish from private replies to group messages.
|
||||
#
|
||||
# Message may be a private reply, so we assign it to 1:1 chat with Alice.
|
||||
assert snapshot.chat == bob_chat_alice
|
||||
|
||||
|
||||
def test_markseen_contact_request(acfactory, tmp_path):
|
||||
"""
|
||||
Test that seen status is synchronized for contact request messages
|
||||
even though read receipt is not sent.
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
# Bob sets up a second device.
|
||||
bob.export_backup(tmp_path)
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
bob2 = acfactory.get_unconfigured_account()
|
||||
bob2.import_backup(files[0])
|
||||
bob2.start_io()
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.send_text("Hello Bob!")
|
||||
|
||||
message = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id)
|
||||
message2 = bob2.get_message_by_id(bob2.wait_for_incoming_msg_event().msg_id)
|
||||
assert message2.get_snapshot().state == MessageState.IN_FRESH
|
||||
|
||||
message.mark_seen()
|
||||
while True:
|
||||
event = bob2.wait_for_event()
|
||||
if event.kind == EventType.MSGS_NOTICED:
|
||||
break
|
||||
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
@@ -25,7 +25,7 @@ deps =
|
||||
black
|
||||
commands =
|
||||
black --quiet --check --diff src/ examples/ tests/
|
||||
ruff src/ examples/ tests/
|
||||
ruff check src/ examples/ tests/
|
||||
|
||||
[pytest]
|
||||
timeout = 300
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.132.1"
|
||||
version = "1.137.4"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
@@ -14,12 +14,12 @@ deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = "..", default-features = false }
|
||||
|
||||
anyhow = "1"
|
||||
env_logger = { version = "0.10.0" }
|
||||
futures-lite = "2.0.0"
|
||||
env_logger = { version = "0.11.3" }
|
||||
futures-lite = "2.3.0"
|
||||
log = "0.4"
|
||||
serde_json = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.33.0", features = ["io-std"] }
|
||||
tokio = { version = "1.37.0", features = ["io-std"] }
|
||||
tokio-util = "0.7.9"
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ deltachat-rpc-server
|
||||
The common use case for this program is to create bindings to use Delta Chat core from programming
|
||||
languages other than Rust, for example:
|
||||
|
||||
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
|
||||
1. Python: https://pypi.org/project/deltachat-rpc-client/
|
||||
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
|
||||
|
||||
Run `deltachat-rpc-server --version` to check the version of the server.
|
||||
|
||||
@@ -68,7 +68,7 @@ async fn main_impl() -> Result<()> {
|
||||
|
||||
log::info!("Creating JSON-RPC API.");
|
||||
let accounts = Arc::new(RwLock::new(accounts));
|
||||
let state = CommandApi::from_arc(accounts.clone());
|
||||
let state = CommandApi::from_arc(accounts.clone()).await;
|
||||
|
||||
let (client, mut out_receiver) = RpcClient::new();
|
||||
let session = RpcSession::new(client.clone(), state.clone());
|
||||
|
||||
8
deltachat-time/Cargo.toml
Normal file
8
deltachat-time/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat-time"
|
||||
version = "1.0.0"
|
||||
description = "Time-related tools"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dependencies]
|
||||
35
deltachat-time/src/lib.rs
Normal file
35
deltachat-time/src/lib.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::sync::RwLock;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
static SYSTEM_TIME_SHIFT: RwLock<Duration> = RwLock::new(Duration::new(0, 0));
|
||||
|
||||
/// Fake struct for mocking `SystemTime::now()` for test purposes. You still need to use
|
||||
/// `SystemTime` as a struct representing a system time.
|
||||
pub struct SystemTimeTools();
|
||||
|
||||
impl SystemTimeTools {
|
||||
pub const UNIX_EPOCH: SystemTime = SystemTime::UNIX_EPOCH;
|
||||
|
||||
pub fn now() -> SystemTime {
|
||||
return SystemTime::now() + *SYSTEM_TIME_SHIFT.read().unwrap();
|
||||
}
|
||||
|
||||
/// Simulates a system clock forward adjustment by `duration`.
|
||||
pub fn shift(duration: Duration) {
|
||||
*SYSTEM_TIME_SHIFT.write().unwrap() += duration;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
SystemTimeTools::shift(Duration::from_secs(60));
|
||||
let t = SystemTimeTools::now();
|
||||
assert!(t > SystemTime::now());
|
||||
}
|
||||
}
|
||||
18
deny.toml
18
deny.toml
@@ -1,5 +1,4 @@
|
||||
[advisories]
|
||||
unmaintained = "allow"
|
||||
ignore = [
|
||||
"RUSTSEC-2020-0071",
|
||||
"RUSTSEC-2022-0093",
|
||||
@@ -10,6 +9,12 @@ ignore = [
|
||||
# There is no fix at the time of writing this (2023-11-28).
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
|
||||
"RUSTSEC-2023-0071",
|
||||
|
||||
# Unmaintained ansi_term
|
||||
"RUSTSEC-2021-0139",
|
||||
|
||||
# Unmaintained encoding
|
||||
"RUSTSEC-2021-0153",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -21,6 +26,7 @@ skip = [
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "base16ct", version = "0.1.1" },
|
||||
{ name = "base64", version = "<0.21" },
|
||||
{ name = "base64", version = "0.21.7" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "block-buffer", version = "<0.10" },
|
||||
{ name = "convert_case", version = "0.4.0" },
|
||||
@@ -32,13 +38,9 @@ skip = [
|
||||
{ name = "digest", version = "<0.10" },
|
||||
{ name = "ed25519-dalek", version = "1.0.1" },
|
||||
{ name = "ed25519", version = "1.5.3" },
|
||||
{ name = "env_logger", version = "0.10.2" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "fd-lock", version = "3.0.13" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "h2", version = "0.3.22" },
|
||||
{ name = "http-body", version = "0.4.5" },
|
||||
{ name = "http", version = "0.2.11" },
|
||||
{ name = "hyper", version = "0.14.27" },
|
||||
{ name = "idna", version = "0.4.0" },
|
||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||
{ name = "pkcs8", version = "0.9.0" },
|
||||
@@ -55,8 +57,10 @@ skip = [
|
||||
{ name = "signature", version = "1.6.4" },
|
||||
{ name = "spin", version = "<0.9.6" },
|
||||
{ name = "spki", version = "0.6.0" },
|
||||
{ name = "sync_wrapper", version = "0.1.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "toml_edit", version = "0.21.1" },
|
||||
{ name = "untrusted", version = "0.7.1" },
|
||||
{ name = "wasi", version = "<0.11" },
|
||||
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
|
||||
@@ -69,6 +73,7 @@ skip = [
|
||||
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
|
||||
{ name = "windows_x86_64_gnu", version = "<0.52" },
|
||||
{ name = "windows_x86_64_msvc", version = "<0.52" },
|
||||
{ name = "winnow", version = "0.5.40" },
|
||||
]
|
||||
|
||||
|
||||
@@ -100,5 +105,4 @@ license-files = [
|
||||
github = [
|
||||
"async-email",
|
||||
"deltachat",
|
||||
"djc",
|
||||
]
|
||||
|
||||
288
flake.lock
generated
Normal file
288
flake.lock
generated
Normal file
@@ -0,0 +1,288 @@
|
||||
{
|
||||
"nodes": {
|
||||
"android": {
|
||||
"inputs": {
|
||||
"devshell": "devshell",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712088936,
|
||||
"narHash": "sha256-mVjeSWQiR/t4UZ9fUawY9OEPAhY1R3meYG+0oh8DUBs=",
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"rev": "2d8181caef279f19c4a33dc694723f89ffc195d4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devshell": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"android",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1711099426,
|
||||
"narHash": "sha256-HzpgM/wc3aqpnHJJ2oDqPBkNsqWbW0WfWUO8lKu8nGk=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "2d45b54ca4a183f2fdcf4b19c895b64fbf620ee8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1713421495,
|
||||
"narHash": "sha256-5vVF9W1tJT+WdfpWAEG76KywktKDAW/71mVmNHEHjac=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "fd47b1f9404fae02a4f38bd9f4b12bad7833c96b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_3": {
|
||||
"inputs": {
|
||||
"systems": "systems_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1713520724,
|
||||
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1710156097,
|
||||
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1711703276,
|
||||
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1713248628,
|
||||
"narHash": "sha256-NLznXB5AOnniUtZsyy/aPWOk8ussTuePp2acb9U+ISA=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5672bc9dbf9d88246ddab5ac454e82318d094bb8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1713562564,
|
||||
"narHash": "sha256-NQpYhgoy0M89g9whRixSwsHb8RFIbwlxeYiVSDwSXJg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "92d295f588631b0db2da509f381b4fb1e74173c5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1713537308,
|
||||
"narHash": "sha256-XtTSSIB2DA6tOv+l0FhvfDMiyCmhoRbNB+0SeInZkbk=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5c24cf2f0a12ad855f444c30b2421d044120c66f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"android": "android",
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils_3",
|
||||
"naersk": "naersk",
|
||||
"nix-filter": "nix-filter",
|
||||
"nixpkgs": "nixpkgs_4"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1713373173,
|
||||
"narHash": "sha256-octd9BFY9G/Gbr4KfwK4itZp4Lx+qvJeRRcYnN+dEH8=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "46702ffc1a02a2ac153f1d1ce619ec917af8f3a6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
541
flake.nix
Normal file
541
flake.nix
Normal file
@@ -0,0 +1,541 @@
|
||||
{
|
||||
description = "Delta Chat core";
|
||||
inputs = {
|
||||
fenix.url = "github:nix-community/fenix";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
nix-filter.url = "github:numtide/nix-filter";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
android.url = "github:tadfisher/android-nixpkgs";
|
||||
};
|
||||
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, android }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
inherit (pkgs.stdenv) isDarwin;
|
||||
fenixPkgs = fenix.packages.${system};
|
||||
naersk' = pkgs.callPackage naersk { };
|
||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||
androidSdk = android.sdk.${system} (sdkPkgs:
|
||||
builtins.attrValues {
|
||||
inherit (sdkPkgs) ndk-24-0-8215888 cmdline-tools-latest;
|
||||
});
|
||||
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/24.0.8215888";
|
||||
|
||||
rustSrc = nix-filter.lib {
|
||||
root = ./.;
|
||||
|
||||
# Include only necessary files
|
||||
# to avoid rebuilds e.g. when README.md or flake.nix changes.
|
||||
include = [
|
||||
./benches
|
||||
./assets
|
||||
./Cargo.lock
|
||||
./Cargo.toml
|
||||
./CMakeLists.txt
|
||||
./CONTRIBUTING.md
|
||||
./deltachat_derive
|
||||
./deltachat-contact-tools
|
||||
./deltachat-ffi
|
||||
./deltachat-jsonrpc
|
||||
./deltachat-ratelimit
|
||||
./deltachat-repl
|
||||
./deltachat-rpc-client
|
||||
./deltachat-time
|
||||
./deltachat-rpc-server
|
||||
./format-flowed
|
||||
./release-date.in
|
||||
./src
|
||||
];
|
||||
exclude = [
|
||||
(nix-filter.lib.matchExt "nix")
|
||||
"flake.lock"
|
||||
];
|
||||
};
|
||||
|
||||
# Map from architecture name to rust targets and nixpkgs targets.
|
||||
arch2targets = {
|
||||
"x86_64-linux" = {
|
||||
rustTarget = "x86_64-unknown-linux-musl";
|
||||
crossTarget = "x86_64-unknown-linux-musl";
|
||||
};
|
||||
"armv7l-linux" = {
|
||||
rustTarget = "armv7-unknown-linux-musleabihf";
|
||||
crossTarget = "armv7l-unknown-linux-musleabihf";
|
||||
};
|
||||
"armv6l-linux" = {
|
||||
rustTarget = "arm-unknown-linux-musleabihf";
|
||||
crossTarget = "armv6l-unknown-linux-musleabihf";
|
||||
};
|
||||
"aarch64-linux" = {
|
||||
rustTarget = "aarch64-unknown-linux-musl";
|
||||
crossTarget = "aarch64-unknown-linux-musl";
|
||||
};
|
||||
"i686-linux" = {
|
||||
rustTarget = "i686-unknown-linux-musl";
|
||||
crossTarget = "i686-unknown-linux-musl";
|
||||
};
|
||||
|
||||
"x86_64-darwin" = {
|
||||
rustTarget = "x86_64-apple-darwin";
|
||||
crossTarget = "x86_64-darwin";
|
||||
};
|
||||
"aarch64-darwin" = {
|
||||
rustTarget = "aarch64-apple-darwin";
|
||||
crossTarget = "aarch64-darwin";
|
||||
};
|
||||
};
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
outputHashes = {
|
||||
"email-0.0.20" = "sha256-rV4Uzqt2Qdrfi5Ti1r+Si1c2iW1kKyWLwOgLkQ5JGGw=";
|
||||
"encoded-words-0.2.0" = "sha256-KK9st0hLFh4dsrnLd6D8lC6pRFFs8W+WpZSGMGJcosk=";
|
||||
"lettre-0.9.2" = "sha256-+hU1cFacyyeC9UGVBpS14BWlJjHy90i/3ynMkKAzclk=";
|
||||
};
|
||||
};
|
||||
mkRustPackage = packageName:
|
||||
naersk'.buildPackage {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
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.
|
||||
};
|
||||
pkgsWin64 = pkgs.pkgsCross.mingwW64;
|
||||
mkWin64RustPackage = packageName:
|
||||
let
|
||||
rustTarget = "x86_64-pc-windows-gnu";
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naerskWin = pkgs.callPackage naersk {
|
||||
cargo = toolchainWin;
|
||||
rustc = toolchainWin;
|
||||
};
|
||||
in
|
||||
naerskWin.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
depsBuildBuild = [
|
||||
pkgsWin64.stdenv.cc
|
||||
pkgsWin64.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
];
|
||||
|
||||
CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
LD = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
};
|
||||
|
||||
pkgsWin32 = pkgs.pkgsCross.mingw32;
|
||||
mkWin32RustPackage = packageName:
|
||||
let
|
||||
rustTarget = "i686-pc-windows-gnu";
|
||||
in
|
||||
let
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naerskWin = pkgs.callPackage naersk {
|
||||
cargo = toolchainWin;
|
||||
rustc = toolchainWin;
|
||||
};
|
||||
|
||||
# Get rid of MCF Gthread library.
|
||||
# See <https://github.com/NixOS/nixpkgs/issues/156343>
|
||||
# and <https://discourse.nixos.org/t/statically-linked-mingw-binaries/38395>
|
||||
# for details.
|
||||
#
|
||||
# Use DWARF-2 instead of SJLJ for exception handling.
|
||||
winCC = pkgsWin32.buildPackages.wrapCC (
|
||||
(pkgsWin32.buildPackages.gcc-unwrapped.override
|
||||
({
|
||||
threadsCross = {
|
||||
model = "win32";
|
||||
package = null;
|
||||
};
|
||||
})).overrideAttrs (oldAttr: {
|
||||
configureFlags = oldAttr.configureFlags ++ [
|
||||
"--disable-sjlj-exceptions --with-dwarf2"
|
||||
];
|
||||
})
|
||||
);
|
||||
in
|
||||
naerskWin.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
depsBuildBuild = [
|
||||
winCC
|
||||
pkgsWin32.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
];
|
||||
|
||||
CC = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
LD = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
};
|
||||
|
||||
mkCrossRustPackage = arch: packageName:
|
||||
let
|
||||
rustTarget = arch2targets."${arch}".rustTarget;
|
||||
crossTarget = arch2targets."${arch}".crossTarget;
|
||||
pkgsCross = import nixpkgs {
|
||||
system = system;
|
||||
crossSystem.config = crossTarget;
|
||||
};
|
||||
in
|
||||
let
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naersk-lib = pkgs.callPackage naersk {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
in
|
||||
naersk-lib.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = rustSrc;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
];
|
||||
|
||||
CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
LD = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
};
|
||||
|
||||
androidAttrs = {
|
||||
armeabi-v7a = {
|
||||
cc = "armv7a-linux-androideabi19-clang";
|
||||
rustTarget = "armv7-linux-androideabi";
|
||||
};
|
||||
arm64-v8a = {
|
||||
cc = "aarch64-linux-android21-clang";
|
||||
rustTarget = "aarch64-linux-android";
|
||||
};
|
||||
};
|
||||
|
||||
mkAndroidRustPackage = arch: packageName:
|
||||
let
|
||||
rustTarget = androidAttrs.${arch}.rustTarget;
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naersk-lib = pkgs.callPackage naersk {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
targetToolchain = "${androidNdkRoot}/toolchains/llvm/prebuilt/linux-x86_64";
|
||||
targetCcName = androidAttrs.${arch}.cc;
|
||||
targetCc = "${targetToolchain}/bin/${targetCcName}";
|
||||
in
|
||||
naersk-lib.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = rustSrc;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${targetCc}";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
];
|
||||
|
||||
CC = "${targetCc}";
|
||||
LD = "${targetCc}";
|
||||
};
|
||||
|
||||
mkAndroidPackages = arch: {
|
||||
"deltachat-rpc-server-${arch}-android" = mkAndroidRustPackage arch "deltachat-rpc-server";
|
||||
"deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
|
||||
};
|
||||
|
||||
mkRustPackages = arch:
|
||||
let
|
||||
rpc-server = mkCrossRustPackage arch "deltachat-rpc-server";
|
||||
in
|
||||
{
|
||||
"deltachat-repl-${arch}" = mkCrossRustPackage arch "deltachat-repl";
|
||||
"deltachat-rpc-server-${arch}" = rpc-server;
|
||||
"deltachat-rpc-server-${arch}-wheel" =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-${arch}-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
rpc-server
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
|
||||
python3 scripts/wheel-rpc-server.py ${arch} tmp/deltachat-rpc-server
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
formatter = pkgs.nixpkgs-fmt;
|
||||
|
||||
packages =
|
||||
mkRustPackages "aarch64-linux" //
|
||||
mkRustPackages "i686-linux" //
|
||||
mkRustPackages "x86_64-linux" //
|
||||
mkRustPackages "armv7l-linux" //
|
||||
mkRustPackages "armv6l-linux" //
|
||||
mkAndroidPackages "armeabi-v7a" //
|
||||
mkAndroidPackages "arm64-v8a" //
|
||||
mkAndroidPackages "x86" //
|
||||
mkAndroidPackages "x86_64" // rec {
|
||||
# Run with `nix run .#deltachat-repl foo.db`.
|
||||
deltachat-repl = mkRustPackage "deltachat-repl";
|
||||
deltachat-rpc-server = mkRustPackage "deltachat-rpc-server";
|
||||
|
||||
deltachat-repl-win64 = mkWin64RustPackage "deltachat-repl";
|
||||
deltachat-rpc-server-win64 = mkWin64RustPackage "deltachat-rpc-server";
|
||||
deltachat-rpc-server-win64-wheel =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-win64-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
deltachat-rpc-server-win64
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${deltachat-rpc-server-win64}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
|
||||
python3 scripts/wheel-rpc-server.py win64 tmp/deltachat-rpc-server.exe
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
|
||||
deltachat-repl-win32 = mkWin32RustPackage "deltachat-repl";
|
||||
deltachat-rpc-server-win32 = mkWin32RustPackage "deltachat-rpc-server";
|
||||
deltachat-rpc-server-win32-wheel =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-win32-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
deltachat-rpc-server-win32
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${deltachat-rpc-server-win32}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
|
||||
python3 scripts/wheel-rpc-server.py win32 tmp/deltachat-rpc-server.exe
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
# Run `nix build .#docs` to get C docs generated in `./result/`.
|
||||
docs =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "docs";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [ pkgs.doxygen ];
|
||||
buildPhase = ''scripts/run-doxygen.sh'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
|
||||
};
|
||||
|
||||
libdeltachat =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "libdeltachat";
|
||||
version = manifest.version;
|
||||
src = rustSrc;
|
||||
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.cmake
|
||||
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 \
|
||||
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
|
||||
'';
|
||||
};
|
||||
|
||||
# Source package for deltachat-rpc-server.
|
||||
# Fake package that downloads Linux version,
|
||||
# needed to install deltachat-rpc-server on Android with `pip`.
|
||||
deltachat-rpc-server-source =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-source";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat-rpc-server-${manifest.version}.tar.gz'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat-rpc-server-${manifest.version}.tar.gz $out'';
|
||||
};
|
||||
|
||||
deltachat-rpc-client =
|
||||
pkgs.python3Packages.buildPythonPackage {
|
||||
pname = "deltachat-rpc-client";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./deltachat-rpc-client;
|
||||
format = "pyproject";
|
||||
propagatedBuildInputs = [
|
||||
pkgs.python3Packages.setuptools
|
||||
pkgs.python3Packages.imap-tools
|
||||
];
|
||||
};
|
||||
|
||||
deltachat-python =
|
||||
pkgs.python3Packages.buildPythonPackage {
|
||||
pname = "deltachat-python";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./python;
|
||||
format = "pyproject";
|
||||
buildInputs = [
|
||||
libdeltachat
|
||||
];
|
||||
nativeBuildInputs = [
|
||||
pkgs.pkg-config
|
||||
];
|
||||
propagatedBuildInputs = [
|
||||
pkgs.python3Packages.setuptools
|
||||
pkgs.python3Packages.pkgconfig
|
||||
pkgs.python3Packages.cffi
|
||||
pkgs.python3Packages.imap-tools
|
||||
pkgs.python3Packages.pluggy
|
||||
pkgs.python3Packages.requests
|
||||
];
|
||||
};
|
||||
python-docs =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "docs";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
buildInputs = [
|
||||
deltachat-python
|
||||
deltachat-rpc-client
|
||||
pkgs.python3Packages.breathe
|
||||
pkgs.python3Packages.sphinx_rtd_theme
|
||||
];
|
||||
nativeBuildInputs = [ pkgs.sphinx ];
|
||||
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
|
||||
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
cargo
|
||||
clippy
|
||||
rustc
|
||||
rustfmt
|
||||
rust-analyzer
|
||||
cargo-deny
|
||||
perl # needed to build vendored OpenSSL
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
608
fuzz/Cargo.lock
generated
608
fuzz/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -29,9 +29,13 @@ module.exports = {
|
||||
DC_DOWNLOAD_FAILURE: 20,
|
||||
DC_DOWNLOAD_IN_PROGRESS: 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE: 30,
|
||||
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200,
|
||||
DC_EVENT_CHATLIST_CHANGED: 2300,
|
||||
DC_EVENT_CHATLIST_ITEM_CHANGED: 2301,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
|
||||
DC_EVENT_CHAT_MODIFIED: 2020,
|
||||
DC_EVENT_CONFIGURE_PROGRESS: 2041,
|
||||
DC_EVENT_CONFIG_SYNCED: 2111,
|
||||
DC_EVENT_CONNECTIVITY_CHANGED: 2100,
|
||||
DC_EVENT_CONTACTS_CHANGED: 2030,
|
||||
DC_EVENT_DELETED_BLOB_FILE: 151,
|
||||
@@ -79,6 +83,7 @@ module.exports = {
|
||||
DC_INFO_EPHEMERAL_TIMER_CHANGED: 10,
|
||||
DC_INFO_GROUP_IMAGE_CHANGED: 3,
|
||||
DC_INFO_GROUP_NAME_CHANGED: 2,
|
||||
DC_INFO_INVALID_UNENCRYPTED_MAIL: 13,
|
||||
DC_INFO_LOCATIONSTREAMING_ENABLED: 8,
|
||||
DC_INFO_LOCATION_ONLY: 9,
|
||||
DC_INFO_MEMBER_ADDED_TO_GROUP: 4,
|
||||
@@ -112,6 +117,9 @@ module.exports = {
|
||||
DC_PROVIDER_STATUS_BROKEN: 3,
|
||||
DC_PROVIDER_STATUS_OK: 1,
|
||||
DC_PROVIDER_STATUS_PREPARATION: 2,
|
||||
DC_PUSH_CONNECTED: 2,
|
||||
DC_PUSH_HEARTBEAT: 1,
|
||||
DC_PUSH_NOT_CONNECTED: 0,
|
||||
DC_QR_ACCOUNT: 250,
|
||||
DC_QR_ADDR: 320,
|
||||
DC_QR_ASK_VERIFYCONTACT: 200,
|
||||
@@ -225,11 +233,13 @@ module.exports = {
|
||||
DC_STR_GROUP_NAME_CHANGED_BY_YOU: 124,
|
||||
DC_STR_IMAGE: 9,
|
||||
DC_STR_INCOMING_MESSAGES: 103,
|
||||
DC_STR_INVALID_UNENCRYPTED_MAIL: 174,
|
||||
DC_STR_LAST_MSG_SENT_SUCCESSFULLY: 111,
|
||||
DC_STR_LOCATION: 66,
|
||||
DC_STR_LOCATION_ENABLED_BY_OTHER: 137,
|
||||
DC_STR_LOCATION_ENABLED_BY_YOU: 136,
|
||||
DC_STR_MESSAGES: 114,
|
||||
DC_STR_MESSAGE_ADD_MEMBER: 173,
|
||||
DC_STR_MSGACTIONBYME: 63,
|
||||
DC_STR_MSGACTIONBYUSER: 62,
|
||||
DC_STR_MSGADDMEMBER: 17,
|
||||
@@ -249,6 +259,7 @@ module.exports = {
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
|
||||
DC_STR_PART_OF_TOTAL_USED: 116,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
|
||||
DC_STR_REACTED_BY: 177,
|
||||
DC_STR_READRCPT: 31,
|
||||
DC_STR_READRCPT_MAILBODY: 32,
|
||||
DC_STR_REMOVE_MEMBER_BY_OTHER: 131,
|
||||
@@ -276,6 +287,7 @@ module.exports = {
|
||||
DC_STR_VIDEOCHAT_INVITE_MSG_BODY: 83,
|
||||
DC_STR_VOICEMESSAGE: 7,
|
||||
DC_STR_WELCOME_MESSAGE: 71,
|
||||
DC_STR_YOU_REACTED: 176,
|
||||
DC_TEXT1_DRAFT: 1,
|
||||
DC_TEXT1_SELF: 3,
|
||||
DC_TEXT1_USERNAME: 2,
|
||||
|
||||
@@ -34,6 +34,10 @@ module.exports = {
|
||||
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
|
||||
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
|
||||
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
|
||||
2111: 'DC_EVENT_CONFIG_SYNCED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED'
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED'
|
||||
}
|
||||
|
||||
@@ -29,9 +29,13 @@ export enum C {
|
||||
DC_DOWNLOAD_FAILURE = 20,
|
||||
DC_DOWNLOAD_IN_PROGRESS = 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE = 30,
|
||||
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
|
||||
DC_EVENT_CHATLIST_CHANGED = 2300,
|
||||
DC_EVENT_CHATLIST_ITEM_CHANGED = 2301,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
|
||||
DC_EVENT_CHAT_MODIFIED = 2020,
|
||||
DC_EVENT_CONFIGURE_PROGRESS = 2041,
|
||||
DC_EVENT_CONFIG_SYNCED = 2111,
|
||||
DC_EVENT_CONNECTIVITY_CHANGED = 2100,
|
||||
DC_EVENT_CONTACTS_CHANGED = 2030,
|
||||
DC_EVENT_DELETED_BLOB_FILE = 151,
|
||||
@@ -79,6 +83,7 @@ export enum C {
|
||||
DC_INFO_EPHEMERAL_TIMER_CHANGED = 10,
|
||||
DC_INFO_GROUP_IMAGE_CHANGED = 3,
|
||||
DC_INFO_GROUP_NAME_CHANGED = 2,
|
||||
DC_INFO_INVALID_UNENCRYPTED_MAIL = 13,
|
||||
DC_INFO_LOCATIONSTREAMING_ENABLED = 8,
|
||||
DC_INFO_LOCATION_ONLY = 9,
|
||||
DC_INFO_MEMBER_ADDED_TO_GROUP = 4,
|
||||
@@ -112,6 +117,9 @@ export enum C {
|
||||
DC_PROVIDER_STATUS_BROKEN = 3,
|
||||
DC_PROVIDER_STATUS_OK = 1,
|
||||
DC_PROVIDER_STATUS_PREPARATION = 2,
|
||||
DC_PUSH_CONNECTED = 2,
|
||||
DC_PUSH_HEARTBEAT = 1,
|
||||
DC_PUSH_NOT_CONNECTED = 0,
|
||||
DC_QR_ACCOUNT = 250,
|
||||
DC_QR_ADDR = 320,
|
||||
DC_QR_ASK_VERIFYCONTACT = 200,
|
||||
@@ -225,11 +233,13 @@ export enum C {
|
||||
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
|
||||
DC_STR_IMAGE = 9,
|
||||
DC_STR_INCOMING_MESSAGES = 103,
|
||||
DC_STR_INVALID_UNENCRYPTED_MAIL = 174,
|
||||
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
|
||||
DC_STR_LOCATION = 66,
|
||||
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
|
||||
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
|
||||
DC_STR_MESSAGES = 114,
|
||||
DC_STR_MESSAGE_ADD_MEMBER = 173,
|
||||
DC_STR_MSGACTIONBYME = 63,
|
||||
DC_STR_MSGACTIONBYUSER = 62,
|
||||
DC_STR_MSGADDMEMBER = 17,
|
||||
@@ -249,6 +259,7 @@ export enum C {
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
|
||||
DC_STR_PART_OF_TOTAL_USED = 116,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
|
||||
DC_STR_REACTED_BY = 177,
|
||||
DC_STR_READRCPT = 31,
|
||||
DC_STR_READRCPT_MAILBODY = 32,
|
||||
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
|
||||
@@ -276,6 +287,7 @@ export enum C {
|
||||
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
|
||||
DC_STR_VOICEMESSAGE = 7,
|
||||
DC_STR_WELCOME_MESSAGE = 71,
|
||||
DC_STR_YOU_REACTED = 176,
|
||||
DC_TEXT1_DRAFT = 1,
|
||||
DC_TEXT1_SELF = 3,
|
||||
DC_TEXT1_USERNAME = 2,
|
||||
@@ -319,6 +331,10 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
|
||||
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
|
||||
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
|
||||
2111: 'DC_EVENT_CONFIG_SYNCED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ export class AccountManager extends EventEmitter {
|
||||
static newTemporary() {
|
||||
let directory = null
|
||||
while (true) {
|
||||
const randomString = Math.random().toString(36).substr(2, 5)
|
||||
const randomString = Math.random().toString(36).substring(2, 5)
|
||||
directory = join(tmpdir(), 'deltachat-' + randomString)
|
||||
if (!existsSync(directory)) break
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#define NAPI_VERSION 4
|
||||
#define NAPI_EXPERIMENTAL
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
@@ -3048,14 +3047,6 @@ NAPI_METHOD(dcn_accounts_select_account) {
|
||||
NAPI_RETURN_UINT32(result);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_accounts_all_work_done) {
|
||||
NAPI_ARGV(1);
|
||||
NAPI_DCN_ACCOUNTS();
|
||||
|
||||
int result = dc_accounts_all_work_done(dcn_accounts->dc_accounts);
|
||||
NAPI_RETURN_INT32(result);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_accounts_start_io) {
|
||||
NAPI_ARGV(1);
|
||||
NAPI_DCN_ACCOUNTS();
|
||||
@@ -3382,7 +3373,6 @@ NAPI_INIT() {
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_get_account);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_get_selected_account);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_select_account);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_all_work_done);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_start_io);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_stop_io);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_maybe_network);
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
// @ts-check
|
||||
import DeltaChat from '../dist'
|
||||
import { DeltaChat } from '../dist/index.js'
|
||||
|
||||
import { deepStrictEqual, strictEqual } from 'assert'
|
||||
import chai, { expect } from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import { EventId2EventName, C } from '../dist/constants'
|
||||
import { EventId2EventName, C } from '../dist/constants.js'
|
||||
import { join } from 'path'
|
||||
import { statSync } from 'fs'
|
||||
import { Context } from '../dist/context'
|
||||
import { Context } from '../dist/context.js'
|
||||
import {fileURLToPath} from 'url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
chai.config.truncateThreshold = 0 // Do not truncate assertion errors.
|
||||
|
||||
@@ -22,6 +26,8 @@ function createTempUser(chatmailDomain) {
|
||||
}
|
||||
|
||||
describe('static tests', function () {
|
||||
this.timeout(60 * 5 * 1000) // increase timeout to 5 min
|
||||
|
||||
it('reverse lookup of events', function () {
|
||||
const eventKeys = Object.keys(EventId2EventName).map((k) => Number(k))
|
||||
const eventValues = Object.values(EventId2EventName)
|
||||
@@ -235,7 +241,7 @@ describe('Basic offline Tests', function () {
|
||||
'delete_device_after',
|
||||
'delete_server_after',
|
||||
'deltachat_core_version',
|
||||
'display_name',
|
||||
'displayname',
|
||||
'download_limit',
|
||||
'e2ee_enabled',
|
||||
'entered_account_settings',
|
||||
@@ -246,6 +252,7 @@ describe('Basic offline Tests', function () {
|
||||
'journal_mode',
|
||||
'key_gen_type',
|
||||
'last_housekeeping',
|
||||
'last_cant_decrypt_outgoing_msgs',
|
||||
'level',
|
||||
'mdns_enabled',
|
||||
'media_quality',
|
||||
@@ -696,7 +703,7 @@ describe('Offline Tests with unconfigured account', function () {
|
||||
})
|
||||
|
||||
describe('Integration tests', function () {
|
||||
this.timeout(60 * 3000) // increase timeout to 1min
|
||||
this.timeout(60 * 5 * 1000) // increase timeout to 5 min
|
||||
|
||||
let [dc, context, accountId, directory, account] = [
|
||||
null,
|
||||
@@ -10,7 +10,6 @@
|
||||
"@types/node": "^20.8.10",
|
||||
"chai": "~4.3.10",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"esm": "^3.2.25",
|
||||
"mocha": "^8.2.1",
|
||||
"node-gyp": "^10.0.0",
|
||||
"prebuildify": "^5.0.1",
|
||||
@@ -53,8 +52,8 @@
|
||||
"prebuildify": "cd node && prebuildify -t 18.0.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
|
||||
"test": "npm run test:lint && npm run test:mocha",
|
||||
"test:lint": "npm run lint",
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.132.1"
|
||||
"version": "1.137.4"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2", "cffi>=1.0.0", "pkgconfig"]
|
||||
requires = ["setuptools>=45", "wheel", "cffi>=1.0.0", "pkgconfig"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.137.4"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
@@ -26,9 +27,6 @@ dependencies = [
|
||||
"pluggy",
|
||||
"requests",
|
||||
]
|
||||
dynamic = [
|
||||
"version"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
|
||||
@@ -44,11 +42,6 @@ deltachat = [
|
||||
"py.typed"
|
||||
]
|
||||
|
||||
[tool.setuptools_scm]
|
||||
root = ".."
|
||||
tag_regex = '^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
|
||||
git_describe_command = "git describe --dirty --tags --long --match v*.*"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Account class implementation."""
|
||||
|
||||
|
||||
import os
|
||||
from array import array
|
||||
from contextlib import contextmanager
|
||||
@@ -376,22 +375,6 @@ class Account:
|
||||
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
|
||||
return (x for x in iter_array(dc_array, lambda x: Message.from_db(self, x)) if x is not None)
|
||||
|
||||
def _wait_next_message_ids(self) -> List[int]:
|
||||
"""Return IDs of all next messages from all chats."""
|
||||
dc_array = ffi.gc(lib.dc_wait_next_msgs(self._dc_context), lib.dc_array_unref)
|
||||
return [lib.dc_array_get_id(dc_array, i) for i in range(lib.dc_array_get_cnt(dc_array))]
|
||||
|
||||
def wait_next_incoming_message(self) -> Message:
|
||||
"""Waits until the next incoming message
|
||||
with ID higher than given is received and returns it."""
|
||||
while True:
|
||||
message_ids = self._wait_next_message_ids()
|
||||
for msg_id in message_ids:
|
||||
message = Message.from_db(self, msg_id)
|
||||
if message and not message.is_from_self() and not message.is_from_device():
|
||||
self.set_config("last_msg_id", str(msg_id))
|
||||
return message
|
||||
|
||||
def create_chat(self, obj) -> Chat:
|
||||
"""Create a 1:1 chat with Account, Contact or e-mail address."""
|
||||
return self.create_contact(obj).create_chat()
|
||||
@@ -478,6 +461,16 @@ class Account:
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
|
||||
|
||||
def resend_messages(self, messages: List[Message]) -> None:
|
||||
"""Resend list of messages.
|
||||
|
||||
:param messages: list of :class:`deltachat.message.Message` object.
|
||||
:returns: None
|
||||
"""
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
if lib.dc_resend_msgs(self._dc_context, msg_ids, len(msg_ids)) != 1:
|
||||
raise ValueError(f"could not resend messages {msg_ids}")
|
||||
|
||||
def delete_messages(self, messages: List[Message]) -> None:
|
||||
"""delete messages (local and remote).
|
||||
|
||||
|
||||
@@ -182,6 +182,12 @@ class FFIEventTracker:
|
||||
print(f"** SECUREJOINT-INVITER PROGRESS {target}", self.account)
|
||||
break
|
||||
|
||||
def wait_securejoin_joiner_progress(self, target):
|
||||
while True:
|
||||
event = self.get_matching("DC_EVENT_SECUREJOIN_JOINER_PROGRESS")
|
||||
if event.data2 >= target:
|
||||
break
|
||||
|
||||
def wait_idle_inbox_ready(self):
|
||||
"""Has to be called after start_io() to wait for fetch_existing_msgs to run
|
||||
so that new messages are not mistaken for old ones:
|
||||
@@ -322,10 +328,12 @@ class EventThread(threading.Thread):
|
||||
elif name == "DC_EVENT_REACTIONS_CHANGED":
|
||||
assert ffi_event.data1 > 0
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
yield "ac_reactions_changed", {"message": msg}
|
||||
if msg is not None:
|
||||
yield "ac_reactions_changed", {"message": msg}
|
||||
elif name == "DC_EVENT_MSG_DELIVERED":
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
yield "ac_message_delivered", {"message": msg}
|
||||
if msg is not None:
|
||||
yield "ac_message_delivered", {"message": msg}
|
||||
elif name == "DC_EVENT_CHAT_MODIFIED":
|
||||
chat = account.get_chat_by_id(ffi_event.data1)
|
||||
yield "ac_chat_modified", {"chat": chat}
|
||||
|
||||
@@ -364,6 +364,9 @@ class Message:
|
||||
else:
|
||||
# load message from db to get a fresh/current state
|
||||
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
|
||||
# Message could be trashed, use the cached object if so.
|
||||
if dc_msg == ffi.NULL:
|
||||
dc_msg = self._dc_msg
|
||||
return lib.dc_msg_get_state(dc_msg)
|
||||
|
||||
def is_in_fresh(self):
|
||||
@@ -392,7 +395,7 @@ class Message:
|
||||
|
||||
def is_outgoing(self):
|
||||
"""Return True if Message is outgoing."""
|
||||
return self._msgstate in (
|
||||
return lib.dc_msg_get_state(self._dc_msg) in (
|
||||
const.DC_STATE_OUT_PREPARING,
|
||||
const.DC_STATE_OUT_PENDING,
|
||||
const.DC_STATE_OUT_FAILED,
|
||||
@@ -484,6 +487,9 @@ class Message:
|
||||
|
||||
# load message from db to get a fresh/current state
|
||||
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
|
||||
# Message could be trashed, use the cached object if so.
|
||||
if dc_msg == ffi.NULL:
|
||||
dc_msg = self._dc_msg
|
||||
return lib.dc_msg_get_download_state(dc_msg)
|
||||
|
||||
def download_full(self) -> None:
|
||||
|
||||
@@ -115,7 +115,7 @@ def pytest_configure(config):
|
||||
deltachat.register_global_plugin(la)
|
||||
|
||||
|
||||
def pytest_report_header(config, startdir):
|
||||
def pytest_report_header(config):
|
||||
info = get_core_info()
|
||||
summary = [
|
||||
"Deltachat core={} sqlite={} journal_mode={}".format(
|
||||
@@ -590,6 +590,20 @@ class ACFactory:
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
def get_protected_chat(self, ac1: Account, ac2: Account):
|
||||
chat = ac1.create_group_chat("Protected Group", verified=True)
|
||||
qr = chat.get_join_qr()
|
||||
ac2.qr_join_chat(qr)
|
||||
ac2._evtracker.wait_securejoin_joiner_progress(1000)
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg is not None
|
||||
assert msg.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg is not None
|
||||
assert "Member Me " in msg.text and " added by " in msg.text
|
||||
return chat
|
||||
|
||||
def introduce_each_other(self, accounts, sending=True):
|
||||
to_wait = []
|
||||
for i, acc in enumerate(accounts):
|
||||
|
||||
@@ -547,13 +547,14 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
assert msg_in.get_sender_contact().addr == ac2_addr
|
||||
|
||||
|
||||
def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
|
||||
def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp):
|
||||
"""Another test for the bug #3836:
|
||||
- Bob has two devices, the second is offline.
|
||||
- Alice creates a verified group and sends a QR invitation to Bob.
|
||||
- Bob joins the group.
|
||||
- Bob's second devices goes online, but sees a contact request instead of the verified group.
|
||||
- The "member added" message is not a system message but a plain text message.
|
||||
- Bob's second device doesn't display the Alice's avatar (bug #5354).
|
||||
- Sending a message fails as the key is missing -- message info says "proper enc-key for <Alice>
|
||||
missing, cannot encrypt".
|
||||
"""
|
||||
@@ -568,6 +569,10 @@ def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
|
||||
ac2_offl.import_self_keys(str(dir))
|
||||
ac2_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: set avatar")
|
||||
avatar_path = data.get_path("d.png")
|
||||
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()
|
||||
@@ -580,11 +585,13 @@ def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
|
||||
ac2_offl.start_io()
|
||||
# Receive "Member Me (<addr>) added by <addr>." message.
|
||||
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
|
||||
contact = msg_in.get_sender_contact()
|
||||
assert msg_in.is_system_message()
|
||||
assert msg_in.get_sender_contact().addr == ac1.get_config("addr")
|
||||
assert contact.addr == ac1.get_config("addr")
|
||||
chat2 = msg_in.chat
|
||||
assert chat2.is_protected()
|
||||
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
|
||||
|
||||
lp.sec("ac2_offl: sending message")
|
||||
msg_out = chat2.send_text("hello")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
import queue
|
||||
import sys
|
||||
import time
|
||||
import base64
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -44,21 +43,21 @@ def test_configure_generate_key(acfactory, lp):
|
||||
lp.sec("ac1: send unencrypted message to ac2")
|
||||
chat.send_text("message1")
|
||||
lp.sec("ac2: waiting for message from ac1")
|
||||
msg_in = ac2.wait_next_incoming_message()
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == "message1"
|
||||
assert not msg_in.is_encrypted()
|
||||
|
||||
lp.sec("ac2: send encrypted message to ac1")
|
||||
msg_in.chat.send_text("message2")
|
||||
lp.sec("ac1: waiting for message from ac2")
|
||||
msg2_in = ac1.wait_next_incoming_message()
|
||||
msg2_in = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg2_in.text == "message2"
|
||||
assert msg2_in.is_encrypted()
|
||||
|
||||
lp.sec("ac1: send encrypted message to ac2")
|
||||
msg2_in.chat.send_text("message3")
|
||||
lp.sec("ac2: waiting for message from ac1")
|
||||
msg3_in = ac2.wait_next_incoming_message()
|
||||
msg3_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg3_in.text == "message3"
|
||||
assert msg3_in.is_encrypted()
|
||||
|
||||
@@ -382,9 +381,24 @@ def test_webxdc_download_on_demand(acfactory, data, lp):
|
||||
assert msgs_changed_event.data2 == 0
|
||||
|
||||
|
||||
def test_enable_mvbox_move(acfactory, lp):
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
lp.sec("ac2: start without mvbox thread")
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=False)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("ac2: configuring mvbox")
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
|
||||
lp.sec("ac1: send message and wait for ac2 to receive it")
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
|
||||
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
|
||||
|
||||
|
||||
def test_mvbox_sentbox_threads(acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True)
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=False)
|
||||
|
||||
lp.sec("ac2: start without mvbox/sentbox threads")
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=False, sentbox_watch=False)
|
||||
@@ -392,10 +406,18 @@ def test_mvbox_sentbox_threads(acfactory, lp):
|
||||
lp.sec("ac2 and ac1: waiting for configuration")
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("ac1: create and configure sentbox")
|
||||
ac1.direct_imap.create_folder("Sent")
|
||||
ac1.set_config("sentbox_watch", "1")
|
||||
|
||||
lp.sec("ac1: send message and wait for ac2 to receive it")
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
|
||||
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
|
||||
|
||||
assert ac1.get_config("configured_mvbox_folder") == "DeltaChat"
|
||||
while ac1.get_config("configured_sentbox_folder") != "Sent":
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
|
||||
def test_move_works(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
@@ -498,6 +520,26 @@ def test_forward_messages(acfactory, lp):
|
||||
assert not chat3.get_messages()
|
||||
|
||||
|
||||
def test_forward_encrypted_to_unencrypted(acfactory, lp):
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
chat = acfactory.get_protected_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: send encrypted message to ac2")
|
||||
txt = "This should be encrypted"
|
||||
chat.send_text(txt)
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == txt
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac2: forward message to ac3 unencrypted")
|
||||
unencrypted_chat = ac2.create_chat(ac3)
|
||||
msg_id = msg.id
|
||||
msg2 = unencrypted_chat.send_msg(msg)
|
||||
assert msg2 == msg
|
||||
assert msg.id != msg_id
|
||||
assert not msg.is_encrypted()
|
||||
|
||||
|
||||
def test_forward_own_message(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -523,6 +565,27 @@ def test_forward_own_message(acfactory, lp):
|
||||
assert msg_in.is_forwarded()
|
||||
|
||||
|
||||
def test_resend_message(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("ac1: send message to ac2")
|
||||
chat1.send_text("message")
|
||||
|
||||
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")
|
||||
assert len(chat2.get_messages()) == chat2_msg_cnt
|
||||
|
||||
|
||||
def test_long_group_name(acfactory, lp):
|
||||
"""See bug https://github.com/deltachat/deltachat-core-rust/issues/3650 "Space added before long
|
||||
group names after MIME serialization/deserialization".
|
||||
@@ -1429,112 +1492,12 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
assert m == msg_in
|
||||
|
||||
|
||||
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
|
||||
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
|
||||
messages are received out of order".
|
||||
|
||||
If the Inbox contains X small messages followed by Y large messages followed by Z small
|
||||
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
|
||||
|
||||
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
|
||||
with online test as follows:
|
||||
- Bob enables download limit and goes offline.
|
||||
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
|
||||
- Bob goes online
|
||||
- Bob first processes a reaction message and throws it away because there is no corresponding
|
||||
message, then processes a partially downloaded message.
|
||||
- As a result, Bob does not see a reaction
|
||||
"""
|
||||
download_limit = 300000
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_addr = ac1.get_config("addr")
|
||||
chat = ac1.create_chat(ac2)
|
||||
ac2.set_config("download_limit", str(download_limit))
|
||||
ac2.stop_io()
|
||||
|
||||
reactions_queue = queue.Queue()
|
||||
|
||||
class InPlugin:
|
||||
@account_hookimpl
|
||||
def ac_reactions_changed(self, message):
|
||||
reactions_queue.put(message)
|
||||
|
||||
ac2.add_account_plugin(InPlugin())
|
||||
|
||||
lp.sec("sending small+large messages from ac1 to ac2")
|
||||
msgs = []
|
||||
msgs.append(chat.send_text("hi"))
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
msgs.append(chat.send_file(str(path)))
|
||||
for m in msgs:
|
||||
ac1._evtracker.wait_msg_delivered(m)
|
||||
|
||||
lp.sec("sending a reaction to the large message from ac1 to ac2")
|
||||
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
|
||||
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
|
||||
# have a later INTERNALDATE.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msgs.append(msgs[-1].send_reaction(react_str))
|
||||
ac1._evtracker.wait_msg_delivered(msgs[-1])
|
||||
|
||||
ac2.start_io()
|
||||
|
||||
lp.sec("wait for ac2 to receive a reaction")
|
||||
msg2 = ac2._evtracker.wait_next_reactions_changed()
|
||||
assert msg2.get_sender_contact().addr == ac1_addr
|
||||
assert msg2.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
|
||||
assert reactions_queue.get() == msg2
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = reactions.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].addr == ac1_addr
|
||||
assert reactions.get_by_contact(contacts[0]) == react_str
|
||||
|
||||
|
||||
def test_reactions_for_a_reordering_move(acfactory, lp):
|
||||
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
|
||||
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
|
||||
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
|
||||
messages they refer to and thus dropped.
|
||||
"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
acfactory.bring_accounts_online()
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac2.stop_io()
|
||||
|
||||
lp.sec("sending message + reaction from ac1 to ac2")
|
||||
msg1 = chat1.send_text("hi")
|
||||
ac1._evtracker.wait_msg_delivered(msg1)
|
||||
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
|
||||
# order by DC, and most (if not all) mail servers provide only seconds precision.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str))
|
||||
|
||||
lp.sec("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||
ac2.direct_imap.connect()
|
||||
for uid in sorted([m.uid for m in ac2.direct_imap.get_all_messages()], reverse=True):
|
||||
ac2.direct_imap.conn.move(uid, "DeltaChat")
|
||||
|
||||
lp.sec("receiving messages by ac2")
|
||||
ac2.start_io()
|
||||
msg2 = ac2._evtracker.wait_next_reactions_changed()
|
||||
assert msg2.text == msg1.text
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = reactions.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].addr == ac1.get_config("addr")
|
||||
assert reactions.get_by_contact(contacts[0]) == react_str
|
||||
|
||||
|
||||
def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
(ac1, some1) = acfactory.get_online_accounts(2)
|
||||
|
||||
lp.sec("create some chat content")
|
||||
chat1 = ac1.create_contact("some1@example.org", name="some1").create_chat()
|
||||
some1_addr = some1.get_config("addr")
|
||||
chat1 = ac1.create_contact(some1_addr, name="some1").create_chat()
|
||||
chat1.send_text("msg1")
|
||||
assert len(ac1.get_contacts(query="some1")) == 1
|
||||
|
||||
@@ -1551,7 +1514,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
contacts = ac.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
assert contact2.addr == some1_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 3
|
||||
@@ -2165,17 +2128,21 @@ def test_delete_multiple_messages(acfactory, lp):
|
||||
|
||||
def test_trash_multiple_messages(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.stop_io()
|
||||
|
||||
# Create the Trash folder on IMAP server
|
||||
# and recreate the account so Trash folder is configured.
|
||||
# Create the Trash folder on IMAP server and configure deletion to it. There was a bug that if
|
||||
# Trash wasn't configured initially, it can't be configured later, let's check this.
|
||||
lp.sec("Creating trash folder")
|
||||
ac2.direct_imap.create_folder("Trash")
|
||||
lp.sec("Creating new accounts")
|
||||
ac2 = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
ac2.set_config("delete_to_trash", "1")
|
||||
assert ac2.get_config("configured_trash_folder")
|
||||
|
||||
lp.sec("Check that Trash can be configured initially as well")
|
||||
ac3 = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
acfactory.bring_accounts_online()
|
||||
assert ac3.get_config("configured_trash_folder")
|
||||
ac3.stop_io()
|
||||
|
||||
ac2.start_io()
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: sending 3 messages")
|
||||
@@ -2190,6 +2157,9 @@ def test_trash_multiple_messages(acfactory, lp):
|
||||
assert msg.text in texts
|
||||
if text != "second":
|
||||
to_delete.append(msg)
|
||||
# ac2 has received some messages, this is impossible w/o the trash folder configured, let's
|
||||
# check the configuration.
|
||||
assert ac2.get_config("configured_trash_folder") == "Trash"
|
||||
|
||||
lp.sec("ac2: deleting all messages except second")
|
||||
assert len(to_delete) == len(texts) - 1
|
||||
|
||||
@@ -52,8 +52,8 @@ class TestOfflineAccountBasic:
|
||||
def test_wrong_db(self, tmp_path):
|
||||
p = tmp_path / "hello.db"
|
||||
p.write_text("123")
|
||||
account = Account(str(p))
|
||||
assert not account.is_open()
|
||||
with pytest.raises(ValueError):
|
||||
_account = Account(str(p))
|
||||
|
||||
def test_os_name(self, tmp_path):
|
||||
p = tmp_path / "hello.db"
|
||||
|
||||
@@ -47,7 +47,7 @@ deps =
|
||||
restructuredtext_lint
|
||||
commands =
|
||||
black --quiet --check --diff setup.py src/deltachat examples/ tests/
|
||||
ruff src/deltachat tests/ examples/
|
||||
ruff check src/deltachat tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
[testenv:mypy]
|
||||
@@ -62,7 +62,8 @@ commands =
|
||||
[testenv:doc]
|
||||
changedir=doc
|
||||
deps =
|
||||
sphinx
|
||||
# Pinned version, workaround for <https://github.com/breathe-doc/breathe/issues/981>
|
||||
sphinx<7.3
|
||||
breathe
|
||||
sphinx_rtd_theme
|
||||
commands =
|
||||
|
||||
@@ -1 +1 @@
|
||||
2023-12-12
|
||||
2024-04-24
|
||||
@@ -35,12 +35,8 @@ and an own build machine.
|
||||
|
||||
- `run_all.sh` builds Python wheels
|
||||
|
||||
- `zig-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Zig toolchain statically linked against musl libc.
|
||||
|
||||
- `android-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Android NDK.
|
||||
|
||||
- `build-python-docs.sh` builds Python documentation into `dist/html/`.
|
||||
|
||||
## Triggering runs on the build machine locally (fast!)
|
||||
|
||||
There is experimental support for triggering a remote Python or Rust test run
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export DCC_RS_TARGET=debug
|
||||
export DCC_RS_DEV="$PWD"
|
||||
cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
python3 -m venv venv
|
||||
venv/bin/pip install ./python
|
||||
venv/bin/pip install ./deltachat-rpc-client
|
||||
venv/bin/pip install sphinx breathe sphinx_rtd_theme
|
||||
venv/bin/pip install ./deltachat-rpc-client
|
||||
venv/bin/sphinx-build -b html -a python/doc/ dist/html
|
||||
@@ -15,55 +15,6 @@ resources:
|
||||
tag_filter: "v*"
|
||||
|
||||
jobs:
|
||||
- name: doxygen
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
trigger: true
|
||||
|
||||
# Build Doxygen documentation
|
||||
- task: build-doxygen
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust
|
||||
outputs:
|
||||
- name: c-docs
|
||||
image_resource:
|
||||
source:
|
||||
repository: alpine
|
||||
type: registry-image
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apk add --no-cache doxygen git
|
||||
cd deltachat-core-rust
|
||||
scripts/run-doxygen.sh
|
||||
cd ..
|
||||
cp -av deltachat-core-rust/deltachat-ffi/html deltachat-core-rust/deltachat-ffi/xml c-docs/
|
||||
|
||||
- task: upload-c-docs
|
||||
config:
|
||||
inputs:
|
||||
- name: c-docs
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: alpine
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apk add --no-cache rsync openssh-client
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete c-docs/html/ delta@c.delta.chat:build-c/master
|
||||
|
||||
- name: python-x86_64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
|
||||
@@ -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.72.0
|
||||
RUST_VERSION=1.77.1
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -220,9 +220,9 @@ if __name__ == "__main__":
|
||||
|
||||
process_dir(Path(sys.argv[1]))
|
||||
|
||||
out_all += "pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| HashMap::from([\n"
|
||||
out_all += "pub(crate) static PROVIDER_DATA: [(&str, &Provider); " + str(len(domains_set)) + "] = [\n";
|
||||
out_all += out_domains
|
||||
out_all += "]));\n\n"
|
||||
out_all += "];\n\n"
|
||||
|
||||
out_all += "pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| HashMap::from([\n"
|
||||
out_all += out_ids
|
||||
|
||||
@@ -73,6 +73,8 @@ def main():
|
||||
"deltachat-jsonrpc/Cargo.toml",
|
||||
"deltachat-rpc-server/Cargo.toml",
|
||||
"deltachat-repl/Cargo.toml",
|
||||
"python/pyproject.toml",
|
||||
"deltachat-rpc-client/pyproject.toml",
|
||||
]
|
||||
try:
|
||||
opts = parser.parse_args()
|
||||
@@ -102,7 +104,7 @@ def main():
|
||||
found = True
|
||||
if not found:
|
||||
raise SystemExit(
|
||||
f"{changelog_name} contains no entry for version: {newversion}"
|
||||
f"CHANGELOG.md contains no entry for version: {newversion}"
|
||||
)
|
||||
|
||||
for toml_filename in toml_list:
|
||||
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=18f714cf73d0bdfb8b013fa344494ab80c92b477
|
||||
REV=2f3db24107e4802c2df0aa0a40f0e144006c0a9b
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build Python wheels for deltachat-rpc-server.
|
||||
Run scripts/zig-rpc-server.sh first."""
|
||||
"""Build Python wheels for deltachat-rpc-server."""
|
||||
from pathlib import Path
|
||||
from wheel.wheelfile import WheelFile
|
||||
import tomllib
|
||||
import tarfile
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
def metadata_contents(version):
|
||||
readme_text = (Path("deltachat-rpc-server") / "README.md").read_text()
|
||||
return f"""Metadata-Version: 2.1
|
||||
Name: deltachat-rpc-server
|
||||
Version: {version}
|
||||
Summary: Delta Chat JSON-RPC server
|
||||
Description-Content-Type: text/markdown
|
||||
|
||||
{readme_text}
|
||||
"""
|
||||
|
||||
|
||||
def build_source_package(version):
|
||||
filename = f"dist/deltachat-rpc-server-{version}.tar.gz"
|
||||
|
||||
def build_source_package(version, filename):
|
||||
with tarfile.open(filename, "w:gz") as pkg:
|
||||
|
||||
def pack(name, contents):
|
||||
@@ -99,7 +101,7 @@ setup(
|
||||
|
||||
|
||||
def build_wheel(version, binary, tag, windows=False):
|
||||
filename = f"dist/deltachat_rpc_server-{version}-{tag}.whl"
|
||||
filename = f"deltachat_rpc_server-{version}-{tag}.whl"
|
||||
|
||||
with WheelFile(filename, "w") as wheel:
|
||||
wheel.write("LICENSE", "deltachat_rpc_server/LICENSE")
|
||||
@@ -126,9 +128,11 @@ def main():
|
||||
Path(binary).chmod(0o755)
|
||||
wheel.write(
|
||||
binary,
|
||||
"deltachat_rpc_server/deltachat-rpc-server.exe"
|
||||
if windows
|
||||
else "deltachat_rpc_server/deltachat-rpc-server",
|
||||
(
|
||||
"deltachat_rpc_server/deltachat-rpc-server.exe"
|
||||
if windows
|
||||
else "deltachat_rpc_server/deltachat-rpc-server"
|
||||
),
|
||||
)
|
||||
wheel.writestr(
|
||||
f"deltachat_rpc_server-{version}.dist-info/METADATA",
|
||||
@@ -144,54 +148,41 @@ def main():
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
with open("deltachat-rpc-server/Cargo.toml", "rb") as f:
|
||||
cargo_toml = tomllib.load(f)
|
||||
version = cargo_toml["package"]["version"]
|
||||
Path("dist").mkdir(exist_ok=True)
|
||||
build_source_package(version)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-x86_64-linux",
|
||||
"py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-armv7-linux",
|
||||
"py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-aarch64-linux",
|
||||
"py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-i686-linux",
|
||||
"py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
|
||||
)
|
||||
|
||||
arch2tags = {
|
||||
"x86_64-linux": "manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
|
||||
"armv7l-linux": "linux_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
|
||||
"armv6l-linux": "linux_armv6l",
|
||||
"aarch64-linux": "manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
|
||||
"i686-linux": "manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
|
||||
"win64": "win_amd64",
|
||||
"win32": "win32",
|
||||
# macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-x86_64-macos",
|
||||
"py3-none-macosx_10_7_x86_64",
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-aarch64-macos",
|
||||
"py3-none-macosx_11_0_arm64",
|
||||
)
|
||||
"x86_64-darwin": "macosx_10_7_x86_64",
|
||||
"aarch64-darwin": "macosx_11_0_arm64",
|
||||
}
|
||||
|
||||
build_wheel(
|
||||
version, "dist/deltachat-rpc-server-win32.exe", "py3-none-win32", windows=True
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-win64.exe",
|
||||
"py3-none-win_amd64",
|
||||
windows=True,
|
||||
)
|
||||
|
||||
def main():
|
||||
with Path("Cargo.toml").open("rb") as fp:
|
||||
cargo_manifest = tomllib.load(fp)
|
||||
version = cargo_manifest["package"]["version"]
|
||||
if sys.argv[1] == "source":
|
||||
filename = f"deltachat-rpc-server-{version}.tar.gz"
|
||||
build_source_package(version, filename)
|
||||
else:
|
||||
arch = sys.argv[1]
|
||||
executable = sys.argv[2]
|
||||
tags = arch2tags[arch]
|
||||
|
||||
if arch in ["win32", "win64"]:
|
||||
build_wheel(
|
||||
version,
|
||||
executable,
|
||||
f"py3-none-{tags}",
|
||||
windows=True,
|
||||
)
|
||||
else:
|
||||
build_wheel(version, executable, f"py3-none-{tags}")
|
||||
|
||||
|
||||
main()
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# /// pyproject
|
||||
# [run]
|
||||
# dependencies = [
|
||||
# "ziglang==0.11.0"
|
||||
# ]
|
||||
# ///
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def flag_filter(flag: str) -> bool:
|
||||
# Workaround for <https://github.com/sfackler/rust-openssl/issues/2043>.
|
||||
if flag == "-latomic":
|
||||
return False
|
||||
|
||||
if flag == "-lc":
|
||||
return False
|
||||
if flag == "-Wl,-melf_i386":
|
||||
return False
|
||||
if "self-contained" in flag and "crt" in flag:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
args = [flag for flag in sys.argv[1:] if flag_filter(flag)]
|
||||
zig_target = os.environ["ZIG_TARGET"]
|
||||
zig_cpu = os.environ.get("ZIG_CPU")
|
||||
if zig_cpu:
|
||||
zig_cpu_args = ["-mcpu=" + zig_cpu]
|
||||
args = [x for x in args if not x.startswith("-march")]
|
||||
else:
|
||||
zig_cpu_args = []
|
||||
|
||||
# Disable atomics and use locks instead in OpenSSL.
|
||||
# Zig toolchains do not provide atomics.
|
||||
# This is a workaround for <https://github.com/deltachat/deltachat-core-rust/issues/4799>
|
||||
args += ["-DBROKEN_CLANG_ATOMICS"]
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"ziglang",
|
||||
"cc",
|
||||
"-target",
|
||||
zig_target,
|
||||
*zig_cpu_args,
|
||||
*args,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
main()
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Build statically linked deltachat-rpc-server using zig.
|
||||
|
||||
set -x
|
||||
set -e
|
||||
|
||||
unset RUSTFLAGS
|
||||
|
||||
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
|
||||
export RUSTUP_TOOLCHAIN=1.72.0
|
||||
|
||||
rustup target add i686-unknown-linux-musl
|
||||
CC="$PWD/scripts/zig-cc" \
|
||||
TARGET_CC="$PWD/scripts/zig-cc" \
|
||||
CARGO_TARGET_I686_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
|
||||
LD="$PWD/scripts/zig-cc" \
|
||||
ZIG_TARGET="x86-linux-musl" \
|
||||
cargo build --release --target i686-unknown-linux-musl -p deltachat-rpc-server --features vendored
|
||||
|
||||
rustup target add armv7-unknown-linux-musleabihf
|
||||
CC="$PWD/scripts/zig-cc" \
|
||||
TARGET_CC="$PWD/scripts/zig-cc" \
|
||||
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER="$PWD/scripts/zig-cc" \
|
||||
LD="$PWD/scripts/zig-cc" \
|
||||
ZIG_TARGET="arm-linux-musleabihf" \
|
||||
ZIG_CPU="generic+v7a+vfp3-d32+thumb2-neon" \
|
||||
cargo build --release --target armv7-unknown-linux-musleabihf -p deltachat-rpc-server --features vendored
|
||||
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
CC="$PWD/scripts/zig-cc" \
|
||||
TARGET_CC="$PWD/scripts/zig-cc" \
|
||||
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
|
||||
LD="$PWD/scripts/zig-cc" \
|
||||
ZIG_TARGET="x86_64-linux-musl" \
|
||||
cargo build --release --target x86_64-unknown-linux-musl -p deltachat-rpc-server --features vendored
|
||||
|
||||
rustup target add aarch64-unknown-linux-musl
|
||||
CC="$PWD/scripts/zig-cc" \
|
||||
TARGET_CC="$PWD/scripts/zig-cc" \
|
||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
|
||||
LD="$PWD/scripts/zig-cc" \
|
||||
ZIG_TARGET="aarch64-linux-musl" \
|
||||
cargo build --release --target aarch64-unknown-linux-musl -p deltachat-rpc-server --features vendored
|
||||
|
||||
mkdir -p dist
|
||||
cp target/x86_64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-linux
|
||||
cp target/i686-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-i686-linux
|
||||
cp target/aarch64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-linux
|
||||
cp target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server dist/deltachat-rpc-server-armv7-linux
|
||||
5
spec.md
5
spec.md
@@ -119,8 +119,9 @@ All group members form the member list.
|
||||
To allow different groups with the same members,
|
||||
groups are identified by a group-id.
|
||||
The group-id MUST be created only from the characters
|
||||
`0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`
|
||||
and MUST have a length of at least 11 characters.
|
||||
`0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`,
|
||||
MUST have a length of at least 11 characters
|
||||
and no more than 32 characters.
|
||||
|
||||
Groups MUST have a group-name.
|
||||
The group-name is any non-zero-length UTF-8 string.
|
||||
|
||||
131
src/accounts.rs
131
src/accounts.rs
@@ -5,6 +5,7 @@ use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use futures::future::join_all;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -16,8 +17,9 @@ use tokio::sync::oneshot;
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::context::{Context, ContextBuilder};
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::push::PushSubscriber;
|
||||
use crate::stock_str::StockStrings;
|
||||
|
||||
/// Account manager, that can handle multiple accounts in a single place.
|
||||
@@ -36,6 +38,9 @@ pub struct Accounts {
|
||||
/// This way changing a translation for one context automatically
|
||||
/// changes it for all other contexts.
|
||||
pub(crate) stockstrings: StockStrings,
|
||||
|
||||
/// Push notification subscriber shared between accounts.
|
||||
push_subscriber: PushSubscriber,
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
@@ -72,8 +77,9 @@ impl Accounts {
|
||||
.context("failed to load accounts config")?;
|
||||
let events = Events::new();
|
||||
let stockstrings = StockStrings::new();
|
||||
let push_subscriber = PushSubscriber::new();
|
||||
let accounts = config
|
||||
.load_accounts(&events, &stockstrings, &dir)
|
||||
.load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
|
||||
.await
|
||||
.context("failed to load accounts")?;
|
||||
|
||||
@@ -83,6 +89,7 @@ impl Accounts {
|
||||
accounts,
|
||||
events,
|
||||
stockstrings,
|
||||
push_subscriber,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -119,13 +126,17 @@ impl Accounts {
|
||||
let account_config = self.config.new_account().await?;
|
||||
let dbfile = account_config.dbfile(&self.dir);
|
||||
|
||||
let ctx = Context::new(
|
||||
&dbfile,
|
||||
account_config.id,
|
||||
self.events.clone(),
|
||||
self.stockstrings.clone(),
|
||||
)
|
||||
.await?;
|
||||
let ctx = ContextBuilder::new(dbfile)
|
||||
.with_id(account_config.id)
|
||||
.with_events(self.events.clone())
|
||||
.with_stock_strings(self.stockstrings.clone())
|
||||
.with_push_subscriber(self.push_subscriber.clone())
|
||||
.build()
|
||||
.await?;
|
||||
// Try to open without a passphrase,
|
||||
// but do not return an error if account is passphare-protected.
|
||||
ctx.open("".to_string()).await?;
|
||||
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
|
||||
Ok(account_config.id)
|
||||
@@ -134,14 +145,15 @@ impl Accounts {
|
||||
/// Adds a new closed account.
|
||||
pub async fn add_closed_account(&mut self) -> Result<u32> {
|
||||
let account_config = self.config.new_account().await?;
|
||||
let dbfile = account_config.dbfile(&self.dir);
|
||||
|
||||
let ctx = Context::new_closed(
|
||||
&account_config.dbfile(&self.dir),
|
||||
account_config.id,
|
||||
self.events.clone(),
|
||||
self.stockstrings.clone(),
|
||||
)
|
||||
.await?;
|
||||
let ctx = ContextBuilder::new(dbfile)
|
||||
.with_id(account_config.id)
|
||||
.with_events(self.events.clone())
|
||||
.with_stock_strings(self.stockstrings.clone())
|
||||
.with_push_subscriber(self.push_subscriber.clone())
|
||||
.build()
|
||||
.await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
|
||||
Ok(account_config.id)
|
||||
@@ -241,25 +253,6 @@ impl Accounts {
|
||||
self.accounts.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
|
||||
///
|
||||
/// Returns whether all accounts finished their background work.
|
||||
/// DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
|
||||
///
|
||||
/// iOS can:
|
||||
/// - call dc_start_io() (in case IO was not running)
|
||||
/// - call dc_maybe_network()
|
||||
/// - while dc_accounts_all_work_done() returns false:
|
||||
/// - Wait for DC_EVENT_CONNECTIVITY_CHANGED
|
||||
pub async fn all_work_done(&self) -> bool {
|
||||
for account in self.accounts.values() {
|
||||
if !account.all_work_done().await {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
|
||||
pub async fn start_io(&mut self) {
|
||||
for account in self.accounts.values_mut() {
|
||||
@@ -291,6 +284,42 @@ impl Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a background fetch for all accounts in parallel.
|
||||
///
|
||||
/// This is an auxiliary function and not part of public API.
|
||||
/// Use [Accounts::background_fetch] instead.
|
||||
async fn background_fetch_without_timeout(&self) {
|
||||
async fn background_fetch_and_log_error(account: Context) {
|
||||
if let Err(error) = account.background_fetch().await {
|
||||
warn!(account, "{error:#}");
|
||||
}
|
||||
}
|
||||
|
||||
join_all(
|
||||
self.accounts
|
||||
.values()
|
||||
.cloned()
|
||||
.map(background_fetch_and_log_error),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end,
|
||||
/// process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
pub async fn background_fetch(&self, timeout: std::time::Duration) {
|
||||
if let Err(_err) =
|
||||
tokio::time::timeout(timeout, self.background_fetch_without_timeout()).await
|
||||
{
|
||||
self.emit_event(EventType::Warning(
|
||||
"Background fetch timed out.".to_string(),
|
||||
));
|
||||
}
|
||||
self.emit_event(EventType::AccountsBackgroundFetchDone);
|
||||
}
|
||||
|
||||
/// Emits a single event.
|
||||
pub fn emit_event(&self, event: EventType) {
|
||||
self.events.emit(Event { id: 0, typ: event })
|
||||
@@ -300,6 +329,12 @@ impl Accounts {
|
||||
pub fn get_event_emitter(&self) -> EventEmitter {
|
||||
self.events.get_emitter()
|
||||
}
|
||||
|
||||
/// Sets notification token for Apple Push Notification service.
|
||||
pub async fn set_push_device_token(&mut self, token: &str) -> Result<()> {
|
||||
self.push_subscriber.set_device_token(token).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration file name.
|
||||
@@ -485,24 +520,24 @@ impl Config {
|
||||
&self,
|
||||
events: &Events,
|
||||
stockstrings: &StockStrings,
|
||||
push_subscriber: PushSubscriber,
|
||||
dir: &Path,
|
||||
) -> Result<BTreeMap<u32, Context>> {
|
||||
let mut accounts = BTreeMap::new();
|
||||
|
||||
for account_config in &self.inner.accounts {
|
||||
let ctx = Context::new(
|
||||
&account_config.dbfile(dir),
|
||||
account_config.id,
|
||||
events.clone(),
|
||||
stockstrings.clone(),
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to create context from file {:?}",
|
||||
account_config.dbfile(dir)
|
||||
)
|
||||
})?;
|
||||
let dbfile = account_config.dbfile(dir);
|
||||
let ctx = ContextBuilder::new(dbfile.clone())
|
||||
.with_id(account_config.id)
|
||||
.with_events(events.clone())
|
||||
.with_stock_strings(stockstrings.clone())
|
||||
.with_push_subscriber(push_subscriber.clone())
|
||||
.build()
|
||||
.await
|
||||
.with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
|
||||
// Try to open without a passphrase,
|
||||
// but do not return an error if account is passphare-protected.
|
||||
ctx.open("".to_string()).await?;
|
||||
|
||||
accounts.insert(account_config.id, ctx);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use mailparse::MailHeaderMap;
|
||||
use mailparse::ParsedMail;
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -14,7 +15,6 @@ use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::tools::time;
|
||||
use crate::tools::EmailAddress;
|
||||
|
||||
/// `authres` is short for the Authentication-Results header, defined in
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc8601>, which contains info
|
||||
@@ -216,7 +216,7 @@ async fn update_authservid_candidates(
|
||||
if old_ids != new_ids {
|
||||
let new_config = new_ids.into_iter().collect::<Vec<_>>().join(" ");
|
||||
context
|
||||
.set_config(Config::AuthservIdCandidates, Some(&new_config))
|
||||
.set_config_internal(Config::AuthservIdCandidates, Some(&new_config))
|
||||
.await?;
|
||||
// Updating the authservid candidates may mean that we now consider
|
||||
// emails as "failed" which "passed" previously, so we need to
|
||||
|
||||
147
src/blob.rs
147
src/blob.rs
@@ -5,11 +5,14 @@ use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use futures::StreamExt;
|
||||
use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat};
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::{fs, io};
|
||||
@@ -33,6 +36,12 @@ pub struct BlobObject<'a> {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ImageOutputFormat {
|
||||
Png,
|
||||
Jpeg { quality: u8 },
|
||||
}
|
||||
|
||||
impl<'a> BlobObject<'a> {
|
||||
/// Creates a new blob object with a unique name.
|
||||
///
|
||||
@@ -312,6 +321,30 @@ impl<'a> BlobObject<'a> {
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns path to the stored Base64-decoded blob.
|
||||
///
|
||||
/// If `data` represents an image of known format, this adds the corresponding extension to
|
||||
/// `suggested_file_stem`.
|
||||
pub(crate) async fn store_from_base64(
|
||||
context: &Context,
|
||||
data: &str,
|
||||
suggested_file_stem: &str,
|
||||
) -> Result<String> {
|
||||
let buf = base64::engine::general_purpose::STANDARD.decode(data)?;
|
||||
let ext = if let Ok(format) = image::guess_format(&buf) {
|
||||
if let Some(ext) = format.extensions_str().first() {
|
||||
format!(".{ext}")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let blob =
|
||||
BlobObject::create(context, &format!("{suggested_file_stem}{ext}"), &buf).await?;
|
||||
Ok(blob.as_name().to_string())
|
||||
}
|
||||
|
||||
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> {
|
||||
let blob_abs = self.to_abs_path();
|
||||
|
||||
@@ -388,6 +421,8 @@ impl<'a> BlobObject<'a> {
|
||||
max_bytes: usize,
|
||||
strict_limits: bool,
|
||||
) -> Result<Option<String>> {
|
||||
// Add white background only to avatars to spare the CPU.
|
||||
let mut add_white_bg = img_wh <= constants::BALANCED_AVATAR_SIZE;
|
||||
let mut no_exif = false;
|
||||
let no_exif_ref = &mut no_exif;
|
||||
let res = tokio::task::block_in_place(move || {
|
||||
@@ -421,13 +456,19 @@ impl<'a> BlobObject<'a> {
|
||||
let exceeds_wh = img.width() > img_wh || img.height() > img_wh;
|
||||
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
|
||||
|
||||
let jpeg_quality = 75;
|
||||
let fmt = ImageFormat::from_path(&blob_abs);
|
||||
let ofmt = match fmt {
|
||||
Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
|
||||
_ => {
|
||||
let jpeg_quality = 75;
|
||||
ImageOutputFormat::Jpeg(jpeg_quality)
|
||||
Ok(ImageFormat::Jpeg) => {
|
||||
add_white_bg = false;
|
||||
ImageOutputFormat::Jpeg {
|
||||
quality: jpeg_quality,
|
||||
}
|
||||
}
|
||||
_ => ImageOutputFormat::Jpeg {
|
||||
quality: jpeg_quality,
|
||||
},
|
||||
};
|
||||
// We need to rewrite images with Exif to remove metadata such as location,
|
||||
// camera model, etc.
|
||||
@@ -438,14 +479,18 @@ impl<'a> BlobObject<'a> {
|
||||
let do_scale = exceeds_max_bytes
|
||||
|| strict_limits
|
||||
&& (exceeds_wh
|
||||
|| exif.is_some()
|
||||
&& encoded_img_exceeds_bytes(
|
||||
|| exif.is_some() && {
|
||||
if mem::take(&mut add_white_bg) {
|
||||
self::add_white_bg(&mut img);
|
||||
}
|
||||
encoded_img_exceeds_bytes(
|
||||
context,
|
||||
&img,
|
||||
ofmt.clone(),
|
||||
max_bytes,
|
||||
&mut encoded,
|
||||
)?);
|
||||
)?
|
||||
});
|
||||
|
||||
if do_scale {
|
||||
if !exceeds_wh {
|
||||
@@ -458,6 +503,9 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
|
||||
loop {
|
||||
if mem::take(&mut add_white_bg) {
|
||||
self::add_white_bg(&mut img);
|
||||
}
|
||||
let new_img = img.thumbnail(img_wh, img_wh);
|
||||
|
||||
if encoded_img_exceeds_bytes(
|
||||
@@ -491,7 +539,7 @@ impl<'a> BlobObject<'a> {
|
||||
if do_scale || exif.is_some() {
|
||||
// The file format is JPEG/PNG now, we may have to change the file extension
|
||||
if !matches!(fmt, Ok(ImageFormat::Jpeg))
|
||||
&& matches!(ofmt, ImageOutputFormat::Jpeg(_))
|
||||
&& matches!(ofmt, ImageOutputFormat::Jpeg { .. })
|
||||
{
|
||||
blob_abs = blob_abs.with_extension("jpg");
|
||||
let file_name = blob_abs.file_name().context("No image file name (???)")?;
|
||||
@@ -500,6 +548,9 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
|
||||
if encoded.is_empty() {
|
||||
if mem::take(&mut add_white_bg) {
|
||||
self::add_white_bg(&mut img);
|
||||
}
|
||||
encode_img(&img, ofmt, &mut encoded)?;
|
||||
}
|
||||
|
||||
@@ -643,7 +694,16 @@ fn encode_img(
|
||||
) -> anyhow::Result<()> {
|
||||
encoded.clear();
|
||||
let mut buf = Cursor::new(encoded);
|
||||
img.write_to(&mut buf, fmt)?;
|
||||
match fmt {
|
||||
ImageOutputFormat::Png => img.write_to(&mut buf, ImageFormat::Png)?,
|
||||
ImageOutputFormat::Jpeg { quality } => {
|
||||
let encoder = JpegEncoder::new_with_quality(&mut buf, quality);
|
||||
// Convert image into RGB8 to avoid the error
|
||||
// "The encoder or decoder for Jpeg does not support the color type Rgba8"
|
||||
// (<https://github.com/image-rs/image/issues/2211>).
|
||||
img.clone().into_rgb8().write_with_encoder(encoder)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -669,11 +729,20 @@ fn encoded_img_exceeds_bytes(
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Removes transparency from an image using a white background.
|
||||
fn add_white_bg(img: &mut DynamicImage) {
|
||||
for y in 0..img.height() {
|
||||
for x in 0..img.width() {
|
||||
let mut p = Rgba([255u8, 255, 255, 255]);
|
||||
p.blend(&img.get_pixel(x, y));
|
||||
img.put_pixel(x, y, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use fs::File;
|
||||
use image::{GenericImageView, Pixel};
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{self, create_group_chat, ProtectionStatus};
|
||||
@@ -885,6 +954,40 @@ mod tests {
|
||||
assert!(!stem.contains('?'));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_white_bg() {
|
||||
let t = TestContext::new().await;
|
||||
let bytes0 = include_bytes!("../test-data/image/logo.png").as_slice();
|
||||
let bytes1 = include_bytes!("../test-data/image/avatar900x900.png").as_slice();
|
||||
for (bytes, color) in [
|
||||
(bytes0, [255u8, 255, 255, 255]),
|
||||
(bytes1, [253u8, 198, 0, 255]),
|
||||
] {
|
||||
let avatar_src = t.dir.path().join("avatar.png");
|
||||
fs::write(&avatar_src, bytes).await.unwrap();
|
||||
|
||||
let mut blob = BlobObject::new_from_path(&t, &avatar_src).await.unwrap();
|
||||
let img_wh = 128;
|
||||
let maybe_sticker = &mut false;
|
||||
let strict_limits = true;
|
||||
blob.recode_to_size(
|
||||
&t,
|
||||
blob.to_abs_path(),
|
||||
maybe_sticker,
|
||||
img_wh,
|
||||
20_000,
|
||||
strict_limits,
|
||||
)
|
||||
.unwrap();
|
||||
tokio::task::block_in_place(move || {
|
||||
let img = image::open(blob.to_abs_path()).unwrap();
|
||||
assert!(img.width() == img_wh);
|
||||
assert!(img.height() == img_wh);
|
||||
assert_eq!(img.get_pixel(0, 0), Rgba(color));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_selfavatar_outside_blobdir() {
|
||||
let t = TestContext::new().await;
|
||||
@@ -1105,6 +1208,28 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Tests that RGBA PNG can be recoded into JPEG
|
||||
/// by dropping alpha channel.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_rgba_png_to_jpeg() {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot-rgba.png");
|
||||
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
bytes,
|
||||
"png",
|
||||
false, // no Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
constants::WORSE_IMAGE_SIZE,
|
||||
constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_huge_jpg() {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
|
||||
|
||||
312
src/chat.rs
312
src/chat.rs
@@ -2,13 +2,13 @@
|
||||
|
||||
use std::cmp;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context as _, Result};
|
||||
use deltachat_contact_tools::{strip_rtlo_characters, ContactAddress};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::EnumIter;
|
||||
@@ -16,13 +16,14 @@ use strum_macros::EnumIter;
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::chatlist_events;
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
|
||||
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS,
|
||||
};
|
||||
use crate::contact::{self, Contact, ContactAddress, ContactId, Origin};
|
||||
use crate::contact::{self, Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc;
|
||||
use crate::download::DownloadState;
|
||||
@@ -44,7 +45,7 @@ use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
|
||||
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
|
||||
smeared_time, strip_rtlo_characters, time, IsNoneOrEmpty,
|
||||
smeared_time, time, IsNoneOrEmpty, SystemTime,
|
||||
};
|
||||
use crate::webxdc::WEBXDC_SUFFIX;
|
||||
|
||||
@@ -210,24 +211,11 @@ impl ChatId {
|
||||
}
|
||||
|
||||
/// Returns [`ChatId`] of a chat that `msg` belongs to.
|
||||
///
|
||||
/// Checks that `msg` is assigned to the right chat.
|
||||
pub(crate) fn lookup_by_message(msg: &Message) -> Option<Self> {
|
||||
if msg.chat_id == DC_CHAT_ID_TRASH {
|
||||
return None;
|
||||
}
|
||||
if msg.download_state != DownloadState::Done
|
||||
// TODO (2023-09-12): Added for backward compatibility with versions that did not have
|
||||
// `DownloadState::Undecipherable`. Remove eventually with the comment in
|
||||
// `MimeMessage::from_bytes()`.
|
||||
|| msg
|
||||
.error
|
||||
.as_ref()
|
||||
.filter(|e| e.starts_with("Decrypting failed:"))
|
||||
.is_some()
|
||||
{
|
||||
// If `msg` is not fully downloaded or undecipherable, it may have been assigned to the
|
||||
// wrong chat (they often get assigned to the 1:1 chat with the sender).
|
||||
if msg.download_state == DownloadState::Undecipherable {
|
||||
return None;
|
||||
}
|
||||
Some(msg.chat_id)
|
||||
@@ -309,6 +297,8 @@ impl ChatId {
|
||||
}
|
||||
};
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
@@ -425,6 +415,7 @@ impl ChatId {
|
||||
}
|
||||
}
|
||||
}
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
if sync.into() {
|
||||
// NB: For a 1:1 chat this currently triggers `Contact::block()` on other devices.
|
||||
@@ -447,6 +438,8 @@ impl ChatId {
|
||||
pub(crate) async fn unblock_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
|
||||
self.set_blocked(context, Blocked::Not).await?;
|
||||
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
// TODO: For a 1:1 chat this currently triggers `Contact::unblock()` on other devices.
|
||||
@@ -457,6 +450,7 @@ impl ChatId {
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -500,6 +494,7 @@ impl ChatId {
|
||||
|
||||
if self.set_blocked(context, Blocked::Not).await? {
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
}
|
||||
|
||||
if sync.into() {
|
||||
@@ -542,6 +537,7 @@ impl ChatId {
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
|
||||
// make sure, the receivers will get all keys
|
||||
self.reset_gossiped_timestamp(context).await?;
|
||||
@@ -590,6 +586,7 @@ impl ChatId {
|
||||
if protection_status_modified {
|
||||
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
|
||||
.await?;
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -676,6 +673,8 @@ impl ChatId {
|
||||
.await?;
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
@@ -782,8 +781,11 @@ impl ChatId {
|
||||
.await?;
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
context.set_config(Config::LastHousekeeping, None).await?;
|
||||
context
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
.await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
|
||||
if chat.is_self_talk() {
|
||||
@@ -791,6 +793,7 @@ impl ChatId {
|
||||
msg.text = stock_str::self_deleted_msg_body(context).await;
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1171,6 +1174,15 @@ impl ChatId {
|
||||
Ok(self.get_param(context).await?.exists(Param::Devicetalk))
|
||||
}
|
||||
|
||||
/// Returns chat member list timestamp.
|
||||
pub(crate) async fn get_member_list_timestamp(self, context: &Context) -> Result<i64> {
|
||||
Ok(self
|
||||
.get_param(context)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
async fn parent_query<T, F>(
|
||||
self,
|
||||
context: &Context,
|
||||
@@ -1429,7 +1441,7 @@ impl rusqlite::types::ToSql for ChatId {
|
||||
impl rusqlite::types::FromSql for ChatId {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
i64::column_result(value).and_then(|val| {
|
||||
if 0 <= val && val <= i64::from(std::u32::MAX) {
|
||||
if 0 <= val && val <= i64::from(u32::MAX) {
|
||||
Ok(ChatId::new(val as u32))
|
||||
} else {
|
||||
Err(rusqlite::types::FromSqlError::OutOfRange(val))
|
||||
@@ -1517,7 +1529,7 @@ impl Chat {
|
||||
Ok(contacts) => {
|
||||
if let Some(contact_id) = contacts.first() {
|
||||
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
|
||||
chat_name = contact.get_display_name().to_owned();
|
||||
contact.get_display_name().clone_into(&mut chat_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1788,18 +1800,10 @@ impl Chat {
|
||||
update_msg_id: Option<MsgId>,
|
||||
timestamp: i64,
|
||||
) -> Result<MsgId> {
|
||||
let mut new_references = "".into();
|
||||
let mut to_id = 0;
|
||||
let mut location_id = 0;
|
||||
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
let new_rfc724_mid = {
|
||||
let grpid = match self.typ {
|
||||
Chattype::Group => Some(self.grpid.as_str()),
|
||||
_ => None,
|
||||
};
|
||||
create_outgoing_rfc724_mid(grpid, &from)
|
||||
};
|
||||
let new_rfc724_mid = create_outgoing_rfc724_mid();
|
||||
|
||||
if self.typ == Chattype::Single {
|
||||
if let Some(id) = context
|
||||
@@ -1837,58 +1841,72 @@ impl Chat {
|
||||
// reset encrypt error state eg. for forwarding
|
||||
msg.param.remove(Param::ErroneousE2ee);
|
||||
|
||||
// set "In-Reply-To:" to identify the message to which the composed message is a reply;
|
||||
// set "References:" to identify the "thread" of the conversation;
|
||||
// both according to RFC 5322 3.6.4, page 25
|
||||
//
|
||||
// as self-talks are mainly used to transfer data between devices,
|
||||
// we do not set In-Reply-To/References in this case.
|
||||
if !self.is_self_talk() {
|
||||
if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
|
||||
// We don't filter `OutPending` and `OutFailed` messages because the new message for
|
||||
// which `parent_query()` is done may assume that it will be received in a context
|
||||
// affected by those messages, e.g. they could add new members to a group and the
|
||||
// new message will contain them in "To:". Anyway recipients must be prepared to
|
||||
// orphaned references.
|
||||
self
|
||||
.id
|
||||
.get_parent_mime_headers(context, MessageState::OutPending)
|
||||
.await?
|
||||
{
|
||||
// "In-Reply-To:" is not changed if it is set manually.
|
||||
// This does not affect "References:" header, it will contain "default parent" (the
|
||||
// latest message in the thread) anyway.
|
||||
if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
|
||||
msg.in_reply_to = Some(parent_rfc724_mid.clone());
|
||||
}
|
||||
|
||||
// the whole list of messages referenced may be huge;
|
||||
// only use the oldest and the parent message
|
||||
let parent_references = parent_references
|
||||
.find(' ')
|
||||
.and_then(|n| parent_references.get(..n))
|
||||
.unwrap_or(&parent_references);
|
||||
|
||||
if !parent_references.is_empty() && !parent_rfc724_mid.is_empty() {
|
||||
// angle brackets are added by the mimefactory later
|
||||
new_references = format!("{parent_references} {parent_rfc724_mid}");
|
||||
} else if !parent_references.is_empty() {
|
||||
new_references = parent_references.to_string();
|
||||
} else if !parent_in_reply_to.is_empty() && !parent_rfc724_mid.is_empty() {
|
||||
new_references = format!("{parent_in_reply_to} {parent_rfc724_mid}");
|
||||
} else if !parent_in_reply_to.is_empty() {
|
||||
new_references = parent_in_reply_to;
|
||||
} else {
|
||||
// as a fallback, use our Message-ID, see reasoning below.
|
||||
new_references = new_rfc724_mid.clone();
|
||||
}
|
||||
} else {
|
||||
// this is a top-level message, add our Message-ID as first reference.
|
||||
// as we always try to extract the grpid also from `References:`-header,
|
||||
// this allows group conversations also if smtp-server as outlook change `Message-ID:`-header
|
||||
// (MUAs usually keep the first Message-ID in `References:`-header unchanged).
|
||||
new_references = new_rfc724_mid.clone();
|
||||
// Set "In-Reply-To:" to identify the message to which the composed message is a reply.
|
||||
// Set "References:" to identify the "thread" of the conversation.
|
||||
// Both according to [RFC 5322 3.6.4, page 25](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4).
|
||||
let new_references;
|
||||
if self.is_self_talk() {
|
||||
// As self-talks are mainly used to transfer data between devices,
|
||||
// we do not set In-Reply-To/References in this case.
|
||||
new_references = String::new();
|
||||
} else if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
|
||||
// We don't filter `OutPending` and `OutFailed` messages because the new message for
|
||||
// which `parent_query()` is done may assume that it will be received in a context
|
||||
// affected by those messages, e.g. they could add new members to a group and the
|
||||
// new message will contain them in "To:". Anyway recipients must be prepared to
|
||||
// orphaned references.
|
||||
self
|
||||
.id
|
||||
.get_parent_mime_headers(context, MessageState::OutPending)
|
||||
.await?
|
||||
{
|
||||
// "In-Reply-To:" is not changed if it is set manually.
|
||||
// This does not affect "References:" header, it will contain "default parent" (the
|
||||
// latest message in the thread) anyway.
|
||||
if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
|
||||
msg.in_reply_to = Some(parent_rfc724_mid.clone());
|
||||
}
|
||||
|
||||
// Use parent `In-Reply-To` as a fallback
|
||||
// in case parent message has no `References` header
|
||||
// as specified in RFC 5322:
|
||||
// > If the parent message does not contain
|
||||
// > a "References:" field but does have an "In-Reply-To:" field
|
||||
// > containing a single message identifier, then the "References:" field
|
||||
// > will contain the contents of the parent's "In-Reply-To:" field
|
||||
// > followed by the contents of the parent's "Message-ID:" field (if
|
||||
// > any).
|
||||
let parent_references = if parent_references.is_empty() {
|
||||
parent_in_reply_to
|
||||
} else {
|
||||
parent_references
|
||||
};
|
||||
|
||||
// The whole list of messages referenced may be huge.
|
||||
// Only take 2 recent references and add third from `In-Reply-To`.
|
||||
let mut references_vec: Vec<&str> = parent_references.rsplit(' ').take(2).collect();
|
||||
references_vec.reverse();
|
||||
|
||||
if !parent_rfc724_mid.is_empty()
|
||||
&& !references_vec.contains(&parent_rfc724_mid.as_str())
|
||||
{
|
||||
references_vec.push(&parent_rfc724_mid)
|
||||
}
|
||||
|
||||
if references_vec.is_empty() {
|
||||
// As a fallback, use our Message-ID,
|
||||
// same as in the case of top-level message.
|
||||
new_references = new_rfc724_mid.clone();
|
||||
} else {
|
||||
new_references = references_vec.join(" ");
|
||||
}
|
||||
} else {
|
||||
// This is a top-level message.
|
||||
// Add our Message-ID as first references.
|
||||
// This allows us to identify replies to our message even if
|
||||
// email server such as Outlook changes `Message-ID:` header.
|
||||
// MUAs usually keep the first Message-ID in `References:` header unchanged.
|
||||
new_references = new_rfc724_mid.clone();
|
||||
}
|
||||
|
||||
// add independent location to database
|
||||
@@ -2031,6 +2049,7 @@ impl Chat {
|
||||
msg.id = MsgId::new(u32::try_from(raw_id)?);
|
||||
|
||||
maybe_set_logging_xdc(context, msg, self.id).await?;
|
||||
context.update_webxdc_integration_database(msg).await?;
|
||||
}
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
Ok(msg.id)
|
||||
@@ -2657,6 +2676,11 @@ pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) ->
|
||||
return send_msg_inner(context, chat_id, msg).await;
|
||||
}
|
||||
|
||||
if msg.state != MessageState::Undefined && msg.state != MessageState::OutPreparing {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.update_param(context).await?;
|
||||
}
|
||||
send_msg_inner(context, chat_id, msg).await
|
||||
}
|
||||
|
||||
@@ -2686,10 +2710,12 @@ async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -
|
||||
}
|
||||
|
||||
if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
if !msg.hidden {
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
}
|
||||
|
||||
if msg.param.exists(Param::SetLatitude) {
|
||||
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
|
||||
context.emit_location_changed(Some(ContactId::SELF)).await?;
|
||||
}
|
||||
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
@@ -2730,26 +2756,25 @@ async fn prepare_send_msg(
|
||||
/// The caller has to interrupt SMTP loop or otherwise process new rows.
|
||||
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
|
||||
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
|
||||
|
||||
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
|
||||
Ok(attach_selfavatar) => attach_selfavatar,
|
||||
Err(err) => {
|
||||
warn!(context, "SMTP job cannot get selfavatar-state: {err:#}.");
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let mimefactory = MimeFactory::from_msg(context, msg, attach_selfavatar).await?;
|
||||
|
||||
let mimefactory = MimeFactory::from_msg(context, msg).await?;
|
||||
let attach_selfavatar = mimefactory.attach_selfavatar;
|
||||
let mut recipients = mimefactory.recipients();
|
||||
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
let lowercase_from = from.to_lowercase();
|
||||
|
||||
// Send BCC to self if it is enabled and we are not going to
|
||||
// delete it immediately. `from` must be the last addr, see `receive_imf_inner()` why.
|
||||
// Send BCC to self if it is enabled.
|
||||
//
|
||||
// Previous versions of Delta Chat did not send BCC self
|
||||
// if DeleteServerAfter was set to immediately delete messages
|
||||
// from the server. This is not the case anymore
|
||||
// because BCC-self messages are also used to detect
|
||||
// that message was sent if SMTP server is slow to respond
|
||||
// and connection is frequently lost
|
||||
// before receiving status line.
|
||||
//
|
||||
// `from` must be the last addr, see `receive_imf_inner()` why.
|
||||
if context.get_config_bool(Config::BccSelf).await?
|
||||
&& context.get_config_delete_server_after().await? != Some(0)
|
||||
&& !recipients
|
||||
.iter()
|
||||
.any(|x| x.to_lowercase() == lowercase_from)
|
||||
@@ -2757,6 +2782,11 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
recipients.push(from);
|
||||
}
|
||||
|
||||
// Webxdc integrations are messages, however, shipped with main app and must not be sent out
|
||||
if msg.param.get_int(Param::WebxdcIntegration).is_some() {
|
||||
recipients.clear();
|
||||
}
|
||||
|
||||
if recipients.is_empty() {
|
||||
// may happen eg. for groups with only SELF and bcc_self disabled
|
||||
info!(
|
||||
@@ -2791,12 +2821,26 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
);
|
||||
}
|
||||
|
||||
let now = smeared_time(context);
|
||||
|
||||
if rendered_msg.is_gossiped {
|
||||
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
|
||||
msg.chat_id.set_gossiped_timestamp(context, now).await?;
|
||||
}
|
||||
|
||||
if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
|
||||
// Reject member list synchronisation from older messages. See also
|
||||
// `receive_imf::apply_group_changes()`.
|
||||
msg.chat_id
|
||||
.update_timestamp(
|
||||
context,
|
||||
Param::MemberListTimestamp,
|
||||
now.saturating_add(constants::TIMESTAMP_SENT_TOLERANCE),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(last_added_location_id) = rendered_msg.last_added_location_id {
|
||||
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await {
|
||||
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
|
||||
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
|
||||
}
|
||||
if !msg.hidden {
|
||||
@@ -2815,7 +2859,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
}
|
||||
|
||||
if attach_selfavatar {
|
||||
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
|
||||
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await {
|
||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||
}
|
||||
}
|
||||
@@ -2825,7 +2869,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
msg.update_param(context).await?;
|
||||
}
|
||||
|
||||
msg.subject = rendered_msg.subject.clone();
|
||||
msg.subject.clone_from(&rendered_msg.subject);
|
||||
msg.update_subject(context).await?;
|
||||
let chunk_size = context
|
||||
.get_configured_provider()
|
||||
@@ -3080,7 +3124,9 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
.await?;
|
||||
for chat_id_in_archive in chat_ids_in_archive {
|
||||
context.emit_event(EventType::MsgsNoticed(chat_id_in_archive));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id_in_archive);
|
||||
}
|
||||
chatlist_events::emit_chatlist_item_changed(context, DC_CHAT_ID_ARCHIVED_LINK);
|
||||
} else {
|
||||
let exists = context
|
||||
.sql
|
||||
@@ -3107,6 +3153,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
}
|
||||
|
||||
context.emit_event(EventType::MsgsNoticed(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3174,6 +3221,7 @@ pub(crate) async fn mark_old_messages_as_noticed(
|
||||
|
||||
for c in changed_chats {
|
||||
context.emit_event(EventType::MsgsNoticed(c));
|
||||
chatlist_events::emit_chatlist_item_changed(context, c);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -3336,6 +3384,8 @@ pub async fn create_group_chat(
|
||||
}
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
|
||||
if protect == ProtectionStatus::Protected {
|
||||
chat_id
|
||||
@@ -3423,11 +3473,14 @@ pub(crate) async fn create_broadcast_list_ex(
|
||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
if sync.into() {
|
||||
let id = SyncId::Grpid(grpid);
|
||||
let action = SyncAction::CreateBroadcast(chat_name);
|
||||
self::sync(context, id, action).await.log_err(context).ok();
|
||||
}
|
||||
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
@@ -3639,7 +3692,7 @@ pub enum MuteDuration {
|
||||
Forever,
|
||||
|
||||
/// Chat is muted for a limited period of time.
|
||||
Until(SystemTime),
|
||||
Until(std::time::SystemTime),
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for MuteDuration {
|
||||
@@ -3698,6 +3751,7 @@ pub(crate) async fn set_muted_ex(
|
||||
.await
|
||||
.context(format!("Failed to set mute duration for {chat_id}"))?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
chat.sync(context, SyncAction::SetMuted(duration))
|
||||
@@ -3858,6 +3912,7 @@ async fn rename_ex(
|
||||
sync = Nosync;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
@@ -3918,6 +3973,7 @@ pub async fn set_chat_profile_image(
|
||||
context.emit_msgs_changed(chat_id, msg.id);
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4063,6 +4119,9 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
msg.timestamp_sort = create_smeared_timestamp(context);
|
||||
// note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
|
||||
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
|
||||
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
@@ -4133,7 +4192,7 @@ pub async fn add_device_msg_with_importance(
|
||||
if let Some(msg) = msg {
|
||||
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
|
||||
|
||||
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
|
||||
let timestamp_sent = create_smeared_timestamp(context);
|
||||
@@ -4252,7 +4311,9 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
context.set_config(Config::QuotaExceeding, None).await?;
|
||||
context
|
||||
.set_config_internal(Config::QuotaExceeding, None)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4271,7 +4332,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
parent: Option<&Message>,
|
||||
from_id: Option<ContactId>,
|
||||
) -> Result<MsgId> {
|
||||
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
|
||||
|
||||
let mut param = Params::new();
|
||||
@@ -4454,9 +4515,8 @@ impl Context {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chatlist::{get_archived_cnt, Chatlist};
|
||||
use crate::chatlist::get_archived_cnt;
|
||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
|
||||
use crate::contact::{Contact, ContactAddress};
|
||||
use crate::message::delete_msgs;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{sync, TestContext, TestContextManager};
|
||||
@@ -4754,9 +4814,9 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test simultaneous removal of user from the chat and leaving the group.
|
||||
/// Test parallel removal of user from the chat and leaving the group.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_simultaneous_member_remove() -> Result<()> {
|
||||
async fn test_parallel_member_remove() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = tcm.alice().await;
|
||||
@@ -4787,20 +4847,25 @@ mod tests {
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?;
|
||||
let alice_sent_add_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Alice removes Bob from the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let alice_sent_remove_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob leaves the chat.
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
|
||||
// Bob receives a msg about Alice adding Claire to the group.
|
||||
bob.recv_msg(&alice_sent_add_msg).await;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
// This adds Bob because they left quite long ago.
|
||||
let alice_sent_msg = alice.send_text(alice_chat_id, "What a silence!").await;
|
||||
bob.recv_msg(&alice_sent_msg).await;
|
||||
|
||||
// Test that add message is rewritten.
|
||||
bob.golden_test_chat(bob_chat_id, "chat_test_simultaneous_member_remove")
|
||||
bob.golden_test_chat(bob_chat_id, "chat_test_parallel_member_remove")
|
||||
.await;
|
||||
|
||||
// Alice removes Bob from the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let alice_sent_remove_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob receives a msg about Alice removing him from the group.
|
||||
let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await;
|
||||
|
||||
@@ -4837,8 +4902,13 @@ mod tests {
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, bob_fiona_contact_id).await?;
|
||||
|
||||
// This doesn't add Fiona back because Bob just removed them.
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Welcome, Fiona!").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Welcome back, Fiona!").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
bob.golden_test_chat(bob_chat_id, "chat_test_msg_with_implicit_member_add")
|
||||
.await;
|
||||
Ok(())
|
||||
@@ -5912,11 +5982,11 @@ mod tests {
|
||||
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let msg = sent_msg.payload();
|
||||
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 2);
|
||||
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
|
||||
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
|
||||
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 0);
|
||||
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
|
||||
assert_eq!(msg.match_indices("Message-ID: <Mr.").count(), 2);
|
||||
assert_eq!(msg.match_indices("References: <Mr.").count(), 1);
|
||||
let msg = msg.replace("Message-ID: <Mr.", "Message-ID: <XXX");
|
||||
assert_eq!(msg.match_indices("Message-ID: <Mr.").count(), 0);
|
||||
assert_eq!(msg.match_indices("References: <Mr.").count(), 1);
|
||||
|
||||
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
|
||||
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
|
||||
@@ -5933,7 +6003,7 @@ mod tests {
|
||||
send_text_msg(&bob, bob_chat.id, "ho!".to_string()).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
let msg = sent_msg.payload();
|
||||
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
|
||||
let msg = msg.replace("Message-ID: <Mr.", "Message-ID: <XXX");
|
||||
let msg = msg.replace("Chat-", "XXXX-");
|
||||
assert_eq!(msg.match_indices("Chat-").count(), 0);
|
||||
|
||||
@@ -6489,6 +6559,7 @@ mod tests {
|
||||
// Bob receives all messages
|
||||
let bob = TestContext::new_bob().await;
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
let sent1_ts_sent = msg.timestamp_sent;
|
||||
assert_eq!(msg.get_text(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1);
|
||||
@@ -6511,6 +6582,7 @@ mod tests {
|
||||
assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2);
|
||||
let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?;
|
||||
assert_eq!(msg_from.get_addr(), "alice@example.org");
|
||||
assert!(sent1_ts_sent < msg.timestamp_sent);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -416,7 +416,7 @@ impl Chatlist {
|
||||
if chat.id.is_archived_link() {
|
||||
Ok(Default::default())
|
||||
} else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != ContactId::UNDEFINED) {
|
||||
Ok(Summary::new(context, &lastmsg, chat, lastcontact.as_ref()).await)
|
||||
Summary::new_with_reaction_details(context, &lastmsg, chat, lastcontact.as_ref()).await
|
||||
} else {
|
||||
Ok(Summary {
|
||||
text: stock_str::no_messages(context).await,
|
||||
|
||||
352
src/config.rs
352
src/config.rs
@@ -5,13 +5,15 @@ use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumProperty, IntoEnumIterator};
|
||||
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::constants::DC_VERSION_STR;
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::constants::{self, DC_VERSION_STR};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
@@ -291,6 +293,9 @@ pub enum Config {
|
||||
/// Timestamp of the last time housekeeping was run
|
||||
LastHousekeeping,
|
||||
|
||||
/// Timestamp of the last `CantDecryptOutgoingMsgs` notification.
|
||||
LastCantDecryptOutgoingMsgs,
|
||||
|
||||
/// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
|
||||
#[strum(props(default = "60"))]
|
||||
ScanAllFoldersDebounceSecs,
|
||||
@@ -347,6 +352,13 @@ pub enum Config {
|
||||
/// 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
|
||||
/// without storing the email address
|
||||
SelfReportingId,
|
||||
|
||||
/// MsgId of webxdc map integration.
|
||||
WebxdcIntegration,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -354,15 +366,31 @@ impl Config {
|
||||
///
|
||||
/// This must be checked on both sides so that if there are different client versions, the
|
||||
/// synchronisation of a particular option is either done or not done in both directions.
|
||||
/// Moreover, receivers of a config value need to check if a key can be synced because some
|
||||
/// settings (e.g. Avatar path) could otherwise lead to exfiltration of files from a receiver's
|
||||
/// Moreover, receivers of a config value need to check if a key can be synced because if it is
|
||||
/// a file path, it could otherwise lead to exfiltration of files from a receiver's
|
||||
/// device if we assume an attacker to have control of a device in a multi-device setting or if
|
||||
/// multiple users are sharing an account. Another example is `Self::SyncMsgs` itself which
|
||||
/// mustn't be controlled by other devices.
|
||||
pub(crate) fn is_synced(&self) -> bool {
|
||||
// We don't restart IO from the synchronisation code, so this is to be on the safe side.
|
||||
if self.needs_io_restart() {
|
||||
return false;
|
||||
}
|
||||
matches!(
|
||||
self,
|
||||
Self::Displayname | Self::MdnsEnabled | Self::ShowEmails
|
||||
Self::Displayname
|
||||
| Self::MdnsEnabled
|
||||
| Self::ShowEmails
|
||||
| Self::Selfavatar
|
||||
| Self::Selfstatus,
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether the config option needs an IO scheduler restart to take effect.
|
||||
pub(crate) fn needs_io_restart(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Config::MvboxMove | Config::OnlyFetchMvbox | Config::SentboxWatch
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -450,6 +478,15 @@ impl Context {
|
||||
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
|
||||
}
|
||||
|
||||
/// Returns true if sentbox ("Sent" folder) should be watched.
|
||||
pub(crate) async fn should_watch_sentbox(&self) -> Result<bool> {
|
||||
Ok(self.get_config_bool(Config::SentboxWatch).await?
|
||||
&& self
|
||||
.get_config(Config::ConfiguredSentboxFolder)
|
||||
.await?
|
||||
.is_some())
|
||||
}
|
||||
|
||||
/// Gets configured "delete_server_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(0)` means delete
|
||||
@@ -484,51 +521,25 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the given config key.
|
||||
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
|
||||
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
|
||||
self.set_config_ex(key.is_synced().into(), key, value).await
|
||||
/// Executes [`SyncData::Config`] item sent by other device.
|
||||
pub(crate) async fn sync_config(&self, key: &Config, value: &str) -> Result<()> {
|
||||
let config_value;
|
||||
let value = match key {
|
||||
Config::Selfavatar if value.is_empty() => None,
|
||||
Config::Selfavatar => {
|
||||
config_value = BlobObject::store_from_base64(self, value, "avatar").await?;
|
||||
Some(config_value.as_str())
|
||||
}
|
||||
_ => Some(value),
|
||||
};
|
||||
match key.is_synced() {
|
||||
true => self.set_config_ex(Nosync, *key, value).await,
|
||||
false => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn set_config_ex(
|
||||
&self,
|
||||
sync: sync::Sync,
|
||||
key: Config,
|
||||
mut value: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let better_value;
|
||||
fn check_config(key: Config, value: Option<&str>) -> Result<()> {
|
||||
match key {
|
||||
Config::Selfavatar => {
|
||||
self.sql
|
||||
.execute("UPDATE contacts SET selfavatar_sent=0;", ())
|
||||
.await?;
|
||||
match value {
|
||||
Some(value) => {
|
||||
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
|
||||
blob.recode_to_avatar_size(self).await?;
|
||||
self.sql
|
||||
.set_raw_config(key.as_ref(), Some(blob.as_name()))
|
||||
.await?;
|
||||
}
|
||||
None => {
|
||||
self.sql.set_raw_config(key.as_ref(), None).await?;
|
||||
}
|
||||
}
|
||||
self.emit_event(EventType::SelfavatarChanged);
|
||||
}
|
||||
Config::DeleteDeviceAfter => {
|
||||
let ret = self.sql.set_raw_config(key.as_ref(), value).await;
|
||||
// Interrupt ephemeral loop to delete old messages immediately.
|
||||
self.scheduler.interrupt_ephemeral_task().await;
|
||||
ret?
|
||||
}
|
||||
Config::Displayname => {
|
||||
if let Some(v) = value {
|
||||
better_value = improve_single_line_input(v);
|
||||
value = Some(&better_value);
|
||||
}
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
}
|
||||
Config::Socks5Enabled
|
||||
| Config::BccSelf
|
||||
| Config::E2eeEnabled
|
||||
@@ -549,6 +560,82 @@ impl Context {
|
||||
matches!(value, None | Some("0") | Some("1")),
|
||||
"Boolean value must be either 0 or 1"
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the given config key and make it effective.
|
||||
/// This may restart the IO scheduler. If `None` is passed as a value the value is cleared and
|
||||
/// set to the default if there is one.
|
||||
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
|
||||
Self::check_config(key, value)?;
|
||||
|
||||
let _pause = match key.needs_io_restart() {
|
||||
true => self.scheduler.pause(self.clone()).await?,
|
||||
_ => Default::default(),
|
||||
};
|
||||
self.set_config_internal(key, value).await?;
|
||||
if key == Config::SentboxWatch {
|
||||
self.last_full_folder_scan.lock().await.take();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_config_internal(&self, key: Config, value: Option<&str>) -> Result<()> {
|
||||
self.set_config_ex(Sync, key, value).await
|
||||
}
|
||||
|
||||
pub(crate) async fn set_config_ex(
|
||||
&self,
|
||||
sync: sync::Sync,
|
||||
key: Config,
|
||||
mut value: Option<&str>,
|
||||
) -> Result<()> {
|
||||
Self::check_config(key, value)?;
|
||||
let sync = sync == Sync && key.is_synced();
|
||||
let better_value;
|
||||
|
||||
match key {
|
||||
Config::Selfavatar => {
|
||||
self.sql
|
||||
.execute("UPDATE contacts SET selfavatar_sent=0;", ())
|
||||
.await?;
|
||||
match value {
|
||||
Some(path) => {
|
||||
let mut blob = BlobObject::new_from_path(self, path.as_ref()).await?;
|
||||
blob.recode_to_avatar_size(self).await?;
|
||||
self.sql
|
||||
.set_raw_config(key.as_ref(), Some(blob.as_name()))
|
||||
.await?;
|
||||
if sync {
|
||||
let buf = fs::read(blob.to_abs_path()).await?;
|
||||
better_value = base64::engine::general_purpose::STANDARD.encode(buf);
|
||||
value = Some(&better_value);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.sql.set_raw_config(key.as_ref(), None).await?;
|
||||
if sync {
|
||||
better_value = String::new();
|
||||
value = Some(&better_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.emit_event(EventType::SelfavatarChanged);
|
||||
}
|
||||
Config::DeleteDeviceAfter => {
|
||||
let ret = self.sql.set_raw_config(key.as_ref(), value).await;
|
||||
// Interrupt ephemeral loop to delete old messages immediately.
|
||||
self.scheduler.interrupt_ephemeral_task().await;
|
||||
ret?
|
||||
}
|
||||
Config::Displayname => {
|
||||
if let Some(v) = value {
|
||||
better_value = improve_single_line_input(v);
|
||||
value = Some(&better_value);
|
||||
}
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
}
|
||||
Config::Addr => {
|
||||
@@ -556,12 +643,20 @@ impl Context {
|
||||
.set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref())
|
||||
.await?;
|
||||
}
|
||||
Config::MvboxMove => {
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
self.sql
|
||||
.set_raw_config(constants::DC_FOLDERS_CONFIGURED_KEY, None)
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if sync != Sync {
|
||||
if key.is_synced() {
|
||||
self.emit_event(EventType::ConfigSynced { key });
|
||||
}
|
||||
if !sync {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(val) = value else {
|
||||
@@ -576,7 +671,7 @@ impl Context {
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
self.send_sync_msg().await.log_err(self).ok();
|
||||
Box::pin(self.send_sync_msg()).await.log_err(self).ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -588,8 +683,7 @@ impl Context {
|
||||
|
||||
/// Set the given config to a boolean value.
|
||||
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
|
||||
self.set_config(key, if value { Some("1") } else { Some("0") })
|
||||
.await?;
|
||||
self.set_config(key, from_bool(value)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -609,6 +703,11 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a value for use in `Context::set_config_*()` for the given `bool`.
|
||||
pub(crate) fn from_bool(val: bool) -> Option<&'static str> {
|
||||
Some(if val { "1" } else { "0" })
|
||||
}
|
||||
|
||||
// Separate impl block for self address handling
|
||||
impl Context {
|
||||
/// Determine whether the specified addr maps to the/a self addr.
|
||||
@@ -635,13 +734,13 @@ impl Context {
|
||||
let mut secondary_addrs = self.get_all_self_addrs().await?;
|
||||
// never store a primary address also as a secondary
|
||||
secondary_addrs.retain(|a| !addr_cmp(a, primary_new));
|
||||
self.set_config(
|
||||
self.set_config_internal(
|
||||
Config::SecondaryAddrs,
|
||||
Some(secondary_addrs.join(" ").as_str()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.set_config(Config::ConfiguredAddr, Some(primary_new))
|
||||
self.set_config_internal(Config::ConfiguredAddr, Some(primary_new))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -689,13 +788,10 @@ fn get_config_keys_string() -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::string::ToString;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use super::*;
|
||||
use crate::constants;
|
||||
use crate::test_utils::{sync, TestContext};
|
||||
use crate::test_utils::{sync, TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
fn test_to_string() {
|
||||
@@ -866,10 +962,10 @@ mod tests {
|
||||
// Reset to default. Test that it's not synced because defaults may differ across client
|
||||
// versions.
|
||||
alice0.set_config(Config::MdnsEnabled, None).await?;
|
||||
assert!(alice0.get_config_bool(Config::MdnsEnabled).await?);
|
||||
assert_eq!(alice0.get_config_bool(Config::MdnsEnabled).await?, true);
|
||||
alice0.set_config_bool(Config::MdnsEnabled, false).await?;
|
||||
sync(&alice0, &alice1).await;
|
||||
assert!(!alice1.get_config_bool(Config::MdnsEnabled).await?);
|
||||
assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);
|
||||
|
||||
let show_emails = alice0.get_config_bool(Config::ShowEmails).await?;
|
||||
alice0
|
||||
@@ -889,14 +985,150 @@ mod tests {
|
||||
assert!(alice1.get_config_bool(Config::MdnsEnabled).await?);
|
||||
|
||||
// Usual sync scenario.
|
||||
async fn test_config_str(
|
||||
alice0: &TestContext,
|
||||
alice1: &TestContext,
|
||||
key: Config,
|
||||
val: &str,
|
||||
) -> Result<()> {
|
||||
alice0.set_config(key, Some(val)).await?;
|
||||
sync(alice0, alice1).await;
|
||||
assert_eq!(alice1.get_config(key).await?, Some(val.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
test_config_str(&alice0, &alice1, Config::Displayname, "Alice Sync").await?;
|
||||
test_config_str(&alice0, &alice1, Config::Selfstatus, "My status").await?;
|
||||
|
||||
assert!(alice0.get_config(Config::Selfavatar).await?.is_none());
|
||||
let file = alice0.dir.path().join("avatar.png");
|
||||
let bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
alice0
|
||||
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
|
||||
.await?;
|
||||
sync(&alice0, &alice1).await;
|
||||
assert!(alice1
|
||||
.get_config(Config::Selfavatar)
|
||||
.await?
|
||||
.filter(|path| path.ends_with(".png"))
|
||||
.is_some());
|
||||
alice0.set_config(Config::Selfavatar, None).await?;
|
||||
sync(&alice0, &alice1).await;
|
||||
assert!(alice1.get_config(Config::Selfavatar).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sync message mustn't be sent if self-{status,avatar} is changed by a self-sent message.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_sync_on_self_sent_msg() -> 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 status = "Synced via usual message";
|
||||
alice0.set_config(Config::Selfstatus, Some(status)).await?;
|
||||
alice0.pop_sent_msg().await; // Sync message
|
||||
let status1 = "Synced via sync message";
|
||||
alice1.set_config(Config::Selfstatus, Some(status1)).await?;
|
||||
tcm.send_recv(alice0, alice1, "hi Alice!").await;
|
||||
assert_eq!(
|
||||
alice1.get_config(Config::Selfstatus).await?,
|
||||
Some(status.to_string())
|
||||
);
|
||||
sync(alice1, alice0).await;
|
||||
assert_eq!(
|
||||
alice0.get_config(Config::Selfstatus).await?,
|
||||
Some(status1.to_string())
|
||||
);
|
||||
|
||||
// Need a chat with another contact to send self-avatar.
|
||||
let bob = &tcm.bob().await;
|
||||
let a0b_chat_id = tcm.send_recv_accept(bob, alice0, "hi").await.chat_id;
|
||||
let file = alice0.dir.path().join("avatar.png");
|
||||
let bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
alice0
|
||||
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
|
||||
.await?;
|
||||
alice0.pop_sent_msg().await; // Sync message
|
||||
let file = alice1.dir.path().join("avatar.jpg");
|
||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
alice1
|
||||
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
|
||||
.await?;
|
||||
let sent_msg = alice0.send_text(a0b_chat_id, "hi").await;
|
||||
alice1.recv_msg(&sent_msg).await;
|
||||
assert!(alice1
|
||||
.get_config(Config::Selfavatar)
|
||||
.await?
|
||||
.filter(|path| path.ends_with(".png"))
|
||||
.is_some());
|
||||
sync(alice1, alice0).await;
|
||||
assert!(alice0
|
||||
.get_config(Config::Selfavatar)
|
||||
.await?
|
||||
.filter(|path| path.ends_with(".jpg"))
|
||||
.is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_event_config_synced() -> Result<()> {
|
||||
let alice0 = TestContext::new_alice().await;
|
||||
let alice1 = TestContext::new_alice().await;
|
||||
for a in [&alice0, &alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
|
||||
alice0
|
||||
.set_config(Config::Displayname, Some("Alice Sync"))
|
||||
.await?;
|
||||
alice0
|
||||
.evtracker
|
||||
.get_matching(|e| {
|
||||
matches!(
|
||||
e,
|
||||
EventType::ConfigSynced {
|
||||
key: Config::Displayname
|
||||
}
|
||||
)
|
||||
})
|
||||
.await;
|
||||
sync(&alice0, &alice1).await;
|
||||
assert_eq!(
|
||||
alice1.get_config(Config::Displayname).await?,
|
||||
Some("Alice Sync".to_string())
|
||||
);
|
||||
alice1
|
||||
.evtracker
|
||||
.get_matching(|e| {
|
||||
matches!(
|
||||
e,
|
||||
EventType::ConfigSynced {
|
||||
key: Config::Displayname
|
||||
}
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
alice0.set_config(Config::Displayname, None).await?;
|
||||
alice0
|
||||
.evtracker
|
||||
.get_matching(|e| {
|
||||
matches!(
|
||||
e,
|
||||
EventType::ConfigSynced {
|
||||
key: Config::Displayname
|
||||
}
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -16,16 +16,16 @@ mod server_params;
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use futures::FutureExt;
|
||||
use futures_lite::FutureExt as _;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use server_params::{expand_param_vector, ServerParams};
|
||||
use tokio::task;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::config::{self, Config};
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::imap::{session::Session as ImapSession, Imap};
|
||||
use crate::log::LogExt;
|
||||
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
|
||||
use crate::message::{Message, Viewtype};
|
||||
@@ -35,8 +35,9 @@ use crate::smtp::Smtp;
|
||||
use crate::socks::Socks5Config;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::{time, EmailAddress};
|
||||
use crate::tools::time;
|
||||
use crate::{chat, e2ee, provider};
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr, $comment:expr) => {
|
||||
@@ -112,12 +113,13 @@ impl Context {
|
||||
let mut param = LoginParam::load_candidate_params(self).await?;
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
let success = configure(self, &mut param).await;
|
||||
self.set_config(Config::NotifyAboutWrongPw, None).await?;
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, None)
|
||||
.await?;
|
||||
|
||||
on_configure_completed(self, param, old_addr).await?;
|
||||
|
||||
success?;
|
||||
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -355,8 +357,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
let mut smtp_configured = false;
|
||||
let mut errors = Vec::new();
|
||||
for smtp_server in smtp_servers {
|
||||
smtp_param.user = smtp_server.username.clone();
|
||||
smtp_param.server = smtp_server.hostname.clone();
|
||||
smtp_param.user.clone_from(&smtp_server.username);
|
||||
smtp_param.server.clone_from(&smtp_server.hostname);
|
||||
smtp_param.port = smtp_server.port;
|
||||
smtp_param.security = smtp_server.socket;
|
||||
smtp_param.certificate_checks = match smtp_server.strict_tls {
|
||||
@@ -394,7 +396,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
// Configure IMAP
|
||||
|
||||
let mut imap: Option<Imap> = None;
|
||||
let mut imap: Option<(Imap, ImapSession)> = None;
|
||||
let imap_servers: Vec<&ServerParams> = servers
|
||||
.iter()
|
||||
.filter(|params| params.protocol == Protocol::Imap)
|
||||
@@ -402,8 +404,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
let imap_servers_count = imap_servers.len();
|
||||
let mut errors = Vec::new();
|
||||
for (imap_server_index, imap_server) in imap_servers.into_iter().enumerate() {
|
||||
param.imap.user = imap_server.username.clone();
|
||||
param.imap.server = imap_server.hostname.clone();
|
||||
param.imap.user.clone_from(&imap_server.username);
|
||||
param.imap.server.clone_from(&imap_server.hostname);
|
||||
param.imap.port = imap_server.port;
|
||||
param.imap.security = imap_server.socket;
|
||||
param.imap.certificate_checks = match imap_server.strict_tls {
|
||||
@@ -432,7 +434,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
|
||||
);
|
||||
}
|
||||
let mut imap = match imap {
|
||||
let (mut imap, mut imap_session) = match imap {
|
||||
Some(imap) => imap,
|
||||
None => bail!(nicer_configuration_error(ctx, errors).await),
|
||||
};
|
||||
@@ -453,9 +455,11 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
let create_mvbox = ctx.should_watch_mvbox().await?;
|
||||
|
||||
imap.configure_folders(ctx, create_mvbox).await?;
|
||||
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
|
||||
.await?;
|
||||
|
||||
imap.select_with_uidvalidity(ctx, "INBOX")
|
||||
imap_session
|
||||
.select_with_uidvalidity(ctx, "INBOX")
|
||||
.await
|
||||
.context("could not read INBOX status")?;
|
||||
|
||||
@@ -473,7 +477,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
// the trailing underscore is correct
|
||||
param.save_as_configured_params(ctx).await?;
|
||||
ctx.set_config(Config::ConfiguredTimestamp, Some(&time().to_string()))
|
||||
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
|
||||
.await?;
|
||||
|
||||
progress!(ctx, 920);
|
||||
@@ -481,7 +485,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
e2ee::ensure_secret_key_exists(ctx).await?;
|
||||
info!(ctx, "key generation completed");
|
||||
|
||||
ctx.set_config_bool(Config::FetchedExistingMsgs, false)
|
||||
ctx.set_config_internal(Config::FetchedExistingMsgs, config::from_bool(false))
|
||||
.await?;
|
||||
ctx.scheduler.interrupt_inbox().await;
|
||||
|
||||
@@ -575,7 +579,7 @@ async fn try_imap_one_param(
|
||||
socks5_config: &Option<Socks5Config>,
|
||||
addr: &str,
|
||||
provider_strict_tls: bool,
|
||||
) -> Result<Imap, ConfigurationError> {
|
||||
) -> Result<(Imap, ImapSession), ConfigurationError> {
|
||||
let inf = format!(
|
||||
"imap: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
|
||||
param.user,
|
||||
@@ -613,9 +617,9 @@ async fn try_imap_one_param(
|
||||
msg: format!("{err:#}"),
|
||||
})
|
||||
}
|
||||
Ok(()) => {
|
||||
Ok(session) => {
|
||||
info!(context, "success: {}", inf);
|
||||
Ok(imap)
|
||||
Ok((imap, session))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,20 +137,11 @@ impl ServerParams {
|
||||
}
|
||||
|
||||
fn expand_strict_tls(self) -> Vec<ServerParams> {
|
||||
if self.strict_tls.is_none() {
|
||||
vec![
|
||||
Self {
|
||||
strict_tls: Some(true), // Strict.
|
||||
..self.clone()
|
||||
},
|
||||
Self {
|
||||
strict_tls: None, // Automatic.
|
||||
..self
|
||||
},
|
||||
]
|
||||
} else {
|
||||
vec![self]
|
||||
}
|
||||
vec![Self {
|
||||
// Strict if not set by the user or provider database.
|
||||
strict_tls: Some(self.strict_tls.unwrap_or(true)),
|
||||
..self
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,31 +153,10 @@ pub(crate) fn expand_param_vector(
|
||||
domain: &str,
|
||||
) -> Vec<ServerParams> {
|
||||
v.into_iter()
|
||||
.map(|params| {
|
||||
if params.socket == Socket::Plain {
|
||||
ServerParams {
|
||||
// Avoid expanding plaintext configuration into configuration with and without
|
||||
// `strict_tls` if `strict_tls` is set to `None` as `strict_tls` is not used for
|
||||
// plaintext connections. Always setting it to "enabled", just in case.
|
||||
strict_tls: Some(true),
|
||||
..params
|
||||
}
|
||||
} else {
|
||||
params
|
||||
}
|
||||
})
|
||||
// The order of expansion is important.
|
||||
//
|
||||
// Ports are expanded the last, so they are changed the first. Username is only changed if
|
||||
// default value (address with domain) didn't work for all available hosts and ports.
|
||||
//
|
||||
// Strict TLS must be expanded first, so we try all configurations with strict TLS first
|
||||
// and only then try again without strict TLS. Otherwise we may lock to wrong hostname
|
||||
// without strict TLS when another hostname with strict TLS is available. For example, if
|
||||
// both smtp.example.net and mail.example.net are running an SMTP server, but both use a
|
||||
// certificate that is only valid for mail.example.net, we want to skip smtp.example.net
|
||||
// and use mail.example.net with strict TLS instead of using smtp.example.net without
|
||||
// strict TLS.
|
||||
.flat_map(|params| params.expand_strict_tls().into_iter())
|
||||
.flat_map(|params| params.expand_usernames(addr).into_iter())
|
||||
.flat_map(|params| params.expand_hostnames(domain).into_iter())
|
||||
@@ -257,22 +227,6 @@ mod tests {
|
||||
username: "foobar".to_string(),
|
||||
strict_tls: Some(true)
|
||||
},
|
||||
ServerParams {
|
||||
protocol: Protocol::Smtp,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 123,
|
||||
socket: Socket::Ssl,
|
||||
username: "foobar".to_string(),
|
||||
strict_tls: None,
|
||||
},
|
||||
ServerParams {
|
||||
protocol: Protocol::Smtp,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 123,
|
||||
socket: Socket::Starttls,
|
||||
username: "foobar".to_string(),
|
||||
strict_tls: None
|
||||
}
|
||||
],
|
||||
);
|
||||
|
||||
@@ -284,7 +238,7 @@ mod tests {
|
||||
port: 123,
|
||||
socket: Socket::Plain,
|
||||
username: "foobar".to_string(),
|
||||
strict_tls: None,
|
||||
strict_tls: Some(true),
|
||||
}],
|
||||
"foobar@example.net",
|
||||
"example.net",
|
||||
|
||||
@@ -206,6 +206,8 @@ pub(crate) const WORSE_AVATAR_SIZE: u32 = 128;
|
||||
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
|
||||
pub const WORSE_IMAGE_SIZE: u32 = 640;
|
||||
|
||||
// Key for the folder configuration version (see below).
|
||||
pub(crate) const DC_FOLDERS_CONFIGURED_KEY: &str = "folders_configured";
|
||||
// this value can be increased if the folder configuration is changed and must be redone on next program start
|
||||
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 4;
|
||||
|
||||
@@ -214,6 +216,13 @@ pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 4;
|
||||
// `max_smtp_rcpt_to` in the provider db.
|
||||
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
|
||||
|
||||
/// How far the last quota check needs to be in the past to be checked by the background function (in seconds).
|
||||
pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60; // 12 hours
|
||||
|
||||
/// How far in the future the sender timestamp of a message is allowed to be, in seconds. Also used
|
||||
/// in the group membership consistency algo to reject outdated membership changes.
|
||||
pub(crate) const TIMESTAMP_SENT_TOLERANCE: i64 = 60;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
327
src/contact.rs
327
src/contact.rs
@@ -2,17 +2,18 @@
|
||||
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::BinaryHeap;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
pub use deltachat_contact_tools::may_be_valid_addr;
|
||||
use deltachat_contact_tools::{
|
||||
addr_cmp, addr_normalize, normalize_name, sanitize_name_and_addr, strip_rtlo_characters,
|
||||
ContactAddress,
|
||||
};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rusqlite::OptionalExtension;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::task;
|
||||
@@ -34,60 +35,12 @@ use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::sync::{self, Sync::*};
|
||||
use crate::tools::{
|
||||
duration_to_str, get_abs_path, improve_single_line_input, strip_rtlo_characters, time,
|
||||
EmailAddress,
|
||||
};
|
||||
use crate::{chat, stock_str};
|
||||
use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, SystemTime};
|
||||
use crate::{chat, chatlist_events, stock_str};
|
||||
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
const SEEN_RECENTLY_SECONDS: i64 = 600;
|
||||
|
||||
/// Valid contact address.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ContactAddress(String);
|
||||
|
||||
impl Deref for ContactAddress {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ContactAddress {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContactAddress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ContactAddress {
|
||||
/// Constructs a new contact address from string,
|
||||
/// normalizing and validating it.
|
||||
pub fn new(s: &str) -> Result<Self> {
|
||||
let addr = addr_normalize(s);
|
||||
if !may_be_valid_addr(&addr) {
|
||||
bail!("invalid address {:?}", s);
|
||||
}
|
||||
Ok(Self(addr.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow converting [`ContactAddress`] to an SQLite type.
|
||||
impl rusqlite::types::ToSql for ContactAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Text(self.0.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contact ID, including reserved IDs.
|
||||
///
|
||||
/// Some contact IDs are reserved to identify special contacts. This
|
||||
@@ -761,6 +714,7 @@ impl Contact {
|
||||
if count > 0 {
|
||||
// Chat name updated
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -797,7 +751,9 @@ impl Contact {
|
||||
Ok(row_id)
|
||||
}).await?;
|
||||
|
||||
Ok((ContactId::new(row_id), sth_modified))
|
||||
let contact_id = ContactId::new(row_id);
|
||||
|
||||
Ok((contact_id, sth_modified))
|
||||
}
|
||||
|
||||
/// Add a number of contacts.
|
||||
@@ -1052,55 +1008,52 @@ impl Contact {
|
||||
"Can not provide encryption info for special contact"
|
||||
);
|
||||
|
||||
let mut ret = String::new();
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
||||
let loginparam = LoginParam::load_configured_params(context).await?;
|
||||
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let loginparam = LoginParam::load_configured_params(context).await?;
|
||||
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
|
||||
|
||||
if let Some(peerstate) =
|
||||
peerstate.filter(|peerstate| peerstate.peek_key(false).is_some())
|
||||
{
|
||||
let stock_message = match peerstate.prefer_encrypt {
|
||||
EncryptPreference::Mutual => stock_str::e2e_preferred(context).await,
|
||||
EncryptPreference::NoPreference => stock_str::e2e_available(context).await,
|
||||
EncryptPreference::Reset => stock_str::encr_none(context).await,
|
||||
};
|
||||
let Some(peerstate) = peerstate.filter(|peerstate| peerstate.peek_key(false).is_some())
|
||||
else {
|
||||
return Ok(stock_str::encr_none(context).await);
|
||||
};
|
||||
|
||||
let finger_prints = stock_str::finger_prints(context).await;
|
||||
ret += &format!("{stock_message}.\n{finger_prints}:");
|
||||
let stock_message = match peerstate.prefer_encrypt {
|
||||
EncryptPreference::Mutual => stock_str::e2e_preferred(context).await,
|
||||
EncryptPreference::NoPreference => stock_str::e2e_available(context).await,
|
||||
EncryptPreference::Reset => stock_str::encr_none(context).await,
|
||||
};
|
||||
|
||||
let fingerprint_self = load_self_public_key(context)
|
||||
.await?
|
||||
.fingerprint()
|
||||
.to_string();
|
||||
let fingerprint_other_verified = peerstate
|
||||
.peek_key(true)
|
||||
.map(|k| k.fingerprint().to_string())
|
||||
.unwrap_or_default();
|
||||
let fingerprint_other_unverified = peerstate
|
||||
.peek_key(false)
|
||||
.map(|k| k.fingerprint().to_string())
|
||||
.unwrap_or_default();
|
||||
if loginparam.addr < peerstate.addr {
|
||||
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&peerstate.addr,
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
);
|
||||
} else {
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&peerstate.addr,
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
);
|
||||
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
||||
}
|
||||
} else {
|
||||
ret += &stock_str::encr_none(context).await;
|
||||
}
|
||||
let finger_prints = stock_str::finger_prints(context).await;
|
||||
let mut ret = format!("{stock_message}.\n{finger_prints}:");
|
||||
|
||||
let fingerprint_self = load_self_public_key(context)
|
||||
.await?
|
||||
.fingerprint()
|
||||
.to_string();
|
||||
let fingerprint_other_verified = peerstate
|
||||
.peek_key(true)
|
||||
.map(|k| k.fingerprint().to_string())
|
||||
.unwrap_or_default();
|
||||
let fingerprint_other_unverified = peerstate
|
||||
.peek_key(false)
|
||||
.map(|k| k.fingerprint().to_string())
|
||||
.unwrap_or_default();
|
||||
if loginparam.addr < peerstate.addr {
|
||||
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&peerstate.addr,
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
);
|
||||
} else {
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&peerstate.addr,
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
);
|
||||
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
@@ -1416,46 +1369,6 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns false if addr is an invalid address, otherwise true.
|
||||
pub fn may_be_valid_addr(addr: &str) -> bool {
|
||||
let res = EmailAddress::new(addr);
|
||||
res.is_ok()
|
||||
}
|
||||
|
||||
/// Returns address lowercased,
|
||||
/// with whitespace trimmed and `mailto:` prefix removed.
|
||||
pub fn addr_normalize(addr: &str) -> String {
|
||||
let norm = addr.trim().to_lowercase();
|
||||
|
||||
if norm.starts_with("mailto:") {
|
||||
norm.get(7..).unwrap_or(&norm).to_string()
|
||||
} else {
|
||||
norm
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.is_empty() {
|
||||
strip_rtlo_characters(
|
||||
&captures
|
||||
.get(1)
|
||||
.map_or("".to_string(), |m| normalize_name(m.as_str())),
|
||||
)
|
||||
} else {
|
||||
strip_rtlo_characters(name)
|
||||
},
|
||||
captures
|
||||
.get(2)
|
||||
.map_or("".to_string(), |m| m.as_str().to_string()),
|
||||
)
|
||||
} else {
|
||||
(strip_rtlo_characters(name), addr.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn set_blocked(
|
||||
context: &Context,
|
||||
sync: sync::Sync,
|
||||
@@ -1528,6 +1441,7 @@ WHERE type=? AND id IN (
|
||||
}
|
||||
}
|
||||
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1537,7 +1451,7 @@ WHERE type=? AND id IN (
|
||||
/// as profile images can be set only by receiving messages, this should be always the case, however.
|
||||
///
|
||||
/// For contact SELF, the image is not saved in the contact-database but as Config::Selfavatar;
|
||||
/// this typically happens if we see message with our own profile image, sent from another device.
|
||||
/// this typically happens if we see message with our own profile image.
|
||||
pub(crate) async fn set_profile_image(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
@@ -1550,7 +1464,7 @@ pub(crate) async fn set_profile_image(
|
||||
if contact_id == ContactId::SELF {
|
||||
if was_encrypted {
|
||||
context
|
||||
.set_config(Config::Selfavatar, Some(profile_image))
|
||||
.set_config_ex(Nosync, Config::Selfavatar, Some(profile_image))
|
||||
.await?;
|
||||
} else {
|
||||
info!(context, "Do not use unencrypted selfavatar.");
|
||||
@@ -1563,7 +1477,9 @@ pub(crate) async fn set_profile_image(
|
||||
AvatarAction::Delete => {
|
||||
if contact_id == ContactId::SELF {
|
||||
if was_encrypted {
|
||||
context.set_config(Config::Selfavatar, None).await?;
|
||||
context
|
||||
.set_config_ex(Nosync, Config::Selfavatar, None)
|
||||
.await?;
|
||||
} else {
|
||||
info!(context, "Do not use unencrypted selfavatar deletion.");
|
||||
}
|
||||
@@ -1576,6 +1492,7 @@ pub(crate) async fn set_profile_image(
|
||||
if changed {
|
||||
contact.update_param(context).await?;
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
chatlist_events::emit_chatlist_item_changed_for_contact_chat(context, contact_id).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1595,7 +1512,7 @@ pub(crate) async fn set_status(
|
||||
if contact_id == ContactId::SELF {
|
||||
if encrypted && has_chat_version {
|
||||
context
|
||||
.set_config(Config::Selfstatus, Some(&status))
|
||||
.set_config_ex(Nosync, Config::Selfstatus, Some(&status))
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
@@ -1631,6 +1548,7 @@ pub(crate) async fn update_last_seen(
|
||||
> 0
|
||||
&& timestamp > time() - SEEN_RECENTLY_SECONDS
|
||||
{
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_recently_seen(contact_id, timestamp)
|
||||
@@ -1639,26 +1557,6 @@ pub(crate) async fn update_last_seen(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Normalize a name.
|
||||
///
|
||||
/// - Remove quotes (come from some bad MUA implementations)
|
||||
/// - Trims the resulting string
|
||||
///
|
||||
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
|
||||
pub fn normalize_name(full_name: &str) -> String {
|
||||
let full_name = full_name.trim();
|
||||
if full_name.is_empty() {
|
||||
return full_name.into();
|
||||
}
|
||||
|
||||
match full_name.as_bytes() {
|
||||
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
|
||||
.get(1..full_name.len() - 1)
|
||||
.map_or("".to_string(), |s| s.trim().to_string()),
|
||||
_ => full_name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cat_fingerprint(
|
||||
ret: &mut String,
|
||||
addr: &str,
|
||||
@@ -1682,14 +1580,6 @@ fn cat_fingerprint(
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares two email addresses, normalizing them beforehand.
|
||||
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
|
||||
let norm1 = addr_normalize(addr1);
|
||||
let norm2 = addr_normalize(addr2);
|
||||
|
||||
norm1 == norm2
|
||||
}
|
||||
|
||||
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
||||
book.lines()
|
||||
.collect::<Vec<&str>>()
|
||||
@@ -1730,6 +1620,12 @@ impl RecentlySeenLoop {
|
||||
async fn run(context: Context, interrupt: Receiver<RecentlySeenInterrupt>) {
|
||||
type MyHeapElem = (Reverse<i64>, ContactId);
|
||||
|
||||
let now = SystemTime::now();
|
||||
let now_ts = now
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64;
|
||||
|
||||
// Priority contains all recently seen sorted by the timestamp
|
||||
// when they become not recently seen.
|
||||
//
|
||||
@@ -1740,7 +1636,7 @@ impl RecentlySeenLoop {
|
||||
.query_map(
|
||||
"SELECT id, last_seen FROM contacts
|
||||
WHERE last_seen > ?",
|
||||
(time() - SEEN_RECENTLY_SECONDS,),
|
||||
(now_ts - SEEN_RECENTLY_SECONDS,),
|
||||
|row| {
|
||||
let contact_id: ContactId = row.get("id")?;
|
||||
let last_seen: i64 = row.get("last_seen")?;
|
||||
@@ -1756,7 +1652,6 @@ impl RecentlySeenLoop {
|
||||
|
||||
loop {
|
||||
let now = SystemTime::now();
|
||||
|
||||
let (until, contact_id) =
|
||||
if let Some((Reverse(timestamp), contact_id)) = unseen_queue.peek() {
|
||||
(
|
||||
@@ -1782,6 +1677,11 @@ impl RecentlySeenLoop {
|
||||
// Timeout, notify about contact.
|
||||
if let Some(contact_id) = contact_id {
|
||||
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
|
||||
chatlist_events::emit_chatlist_item_changed_for_contact_chat(
|
||||
&context,
|
||||
*contact_id,
|
||||
)
|
||||
.await;
|
||||
unseen_queue.pop();
|
||||
}
|
||||
}
|
||||
@@ -1799,7 +1699,10 @@ impl RecentlySeenLoop {
|
||||
timestamp,
|
||||
})) => {
|
||||
// Received an interrupt.
|
||||
unseen_queue.push((Reverse(timestamp + SEEN_RECENTLY_SECONDS), contact_id));
|
||||
if contact_id != ContactId::UNDEFINED {
|
||||
unseen_queue
|
||||
.push((Reverse(timestamp + SEEN_RECENTLY_SECONDS), contact_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1811,13 +1714,18 @@ impl RecentlySeenLoop {
|
||||
// Event is already in the past.
|
||||
if let Some(contact_id) = contact_id {
|
||||
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
|
||||
chatlist_events::emit_chatlist_item_changed_for_contact_chat(
|
||||
&context,
|
||||
*contact_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
unseen_queue.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn interrupt(&self, contact_id: ContactId, timestamp: i64) {
|
||||
pub(crate) fn try_interrupt(&self, contact_id: ContactId, timestamp: i64) {
|
||||
self.interrupt_send
|
||||
.try_send(RecentlySeenInterrupt {
|
||||
contact_id,
|
||||
@@ -1826,6 +1734,17 @@ impl RecentlySeenLoop {
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn interrupt(&self, contact_id: ContactId, timestamp: i64) {
|
||||
self.interrupt_send
|
||||
.send(RecentlySeenInterrupt {
|
||||
contact_id,
|
||||
timestamp,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) fn abort(self) {
|
||||
self.handle.abort();
|
||||
}
|
||||
@@ -1833,6 +1752,8 @@ impl RecentlySeenLoop {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use deltachat_contact_tools::may_be_valid_addr;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
|
||||
use crate::chatlist::Chatlist;
|
||||
@@ -1972,18 +1893,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contact_address() -> Result<()> {
|
||||
let alice_addr = "alice@example.org";
|
||||
let contact_address = ContactAddress::new(alice_addr)?;
|
||||
assert_eq!(contact_address.as_ref(), alice_addr);
|
||||
|
||||
let invalid_addr = "<> foobar";
|
||||
assert!(ContactAddress::new(invalid_addr).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_or_lookup() {
|
||||
// add some contacts, this also tests add_address_book()
|
||||
@@ -2807,6 +2716,44 @@ Hi."#;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_was_seen_recently_event() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let recently_seen_loop = RecentlySeenLoop::new(bob.ctx.clone());
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let contacts = chat::get_chat_contacts(&bob, chat.id).await?;
|
||||
|
||||
for _ in 0..2 {
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent_msg = alice.send_text(chat.id, "moin").await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(!contact.was_seen_recently());
|
||||
bob.evtracker.clear_events();
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(contact.was_seen_recently());
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
recently_seen_loop
|
||||
.interrupt(contact.id, contact.last_seen)
|
||||
.await;
|
||||
|
||||
// Wait for `was_seen_recently()` to turn off.
|
||||
bob.evtracker.clear_events();
|
||||
SystemTime::shift(Duration::from_secs(SEEN_RECENTLY_SECONDS as u64 * 2));
|
||||
recently_seen_loop.interrupt(ContactId::UNDEFINED, 0).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(!contact.was_seen_recently());
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verified_by_none() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user