mirror of
https://github.com/chatmail/core.git
synced 2026-04-13 11:40:41 +03:00
Compare commits
403 Commits
link2xt/bu
...
hoc/git-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d4af85cb1 | ||
|
|
f637278cb1 | ||
|
|
a202771d36 | ||
|
|
833d30123c | ||
|
|
b6c0b5b66e | ||
|
|
f30a1a3865 | ||
|
|
3a25d6a44e | ||
|
|
0688895022 | ||
|
|
ce0e5416e6 | ||
|
|
7918a6801e | ||
|
|
6af631e8df | ||
|
|
625ecaa9b5 | ||
|
|
24fe1b9c15 | ||
|
|
ba36d09c70 | ||
|
|
f57be7187e | ||
|
|
6a00338f79 | ||
|
|
16906210e1 | ||
|
|
f9b4540387 | ||
|
|
9755438d0d | ||
|
|
fe9534ed7d | ||
|
|
134c9ada68 | ||
|
|
a3240452ff | ||
|
|
84beb6647d | ||
|
|
ecf7e2d909 | ||
|
|
fcfcf4bbf3 | ||
|
|
c62b6d77b7 | ||
|
|
7e51b9686f | ||
|
|
542ec4cac4 | ||
|
|
64b25d9ec0 | ||
|
|
f91b6fbdf0 | ||
|
|
41445a506e | ||
|
|
798db9d019 | ||
|
|
2e860c32ab | ||
|
|
0e1faed6e5 | ||
|
|
f5de3be977 | ||
|
|
4b0a30eb66 | ||
|
|
7710467571 | ||
|
|
1687794b81 | ||
|
|
158541f05c | ||
|
|
eb28899cd0 | ||
|
|
432046225a | ||
|
|
bf958ce6c1 | ||
|
|
ea8ee4e67d | ||
|
|
edfdbbdc90 | ||
|
|
1b8bfef441 | ||
|
|
514074de8b | ||
|
|
e7aab5c67c | ||
|
|
40484e875e | ||
|
|
fc215ceb63 | ||
|
|
2701c135db | ||
|
|
3a8df3e673 | ||
|
|
fa95b269a5 | ||
|
|
0e9f8c4726 | ||
|
|
a8d4cbd5c1 | ||
|
|
f68a2fc387 | ||
|
|
ede4e8109e | ||
|
|
663df6bdfd | ||
|
|
d97bdd9fd0 | ||
|
|
a806a218bf | ||
|
|
903633f422 | ||
|
|
3c774b02e5 | ||
|
|
4aae48b0a1 | ||
|
|
a8b790a5db | ||
|
|
6a6ceb6875 | ||
|
|
37503dd3e8 | ||
|
|
3f615c8de6 | ||
|
|
2ef5f2eb52 | ||
|
|
f267f6f756 | ||
|
|
538db53887 | ||
|
|
21349abed8 | ||
|
|
d2fb2bb2ca | ||
|
|
0b832fb9de | ||
|
|
179b9ba2cb | ||
|
|
9150e9fb38 | ||
|
|
4716fcef94 | ||
|
|
2b7ee85e30 | ||
|
|
2b8888350b | ||
|
|
4dfe34eedc | ||
|
|
430a71288f | ||
|
|
350509d5d1 | ||
|
|
5403fd849c | ||
|
|
fa87d2e225 | ||
|
|
28a13e98a6 | ||
|
|
b369a30544 | ||
|
|
318ed4e6e1 | ||
|
|
c6a64e8d93 | ||
|
|
efd6937dfa | ||
|
|
36bf1fe3f6 | ||
|
|
4da0e9ac64 | ||
|
|
436766f002 | ||
|
|
d4f2507288 | ||
|
|
28fd27476f | ||
|
|
619b849ce7 | ||
|
|
f1eeb1df8c | ||
|
|
cecc080931 | ||
|
|
11e6a325a2 | ||
|
|
eed8e08145 | ||
|
|
36bec9c295 | ||
|
|
c8988f5a55 | ||
|
|
86c18fb3ae | ||
|
|
2ba4381c39 | ||
|
|
185a0193cc | ||
|
|
1b00334281 | ||
|
|
0eb2f5bf52 | ||
|
|
78d1aa46a1 | ||
|
|
26403a1599 | ||
|
|
a1f112470e | ||
|
|
2ab7a37d85 | ||
|
|
4b933ed2ef | ||
|
|
0d1c12115d | ||
|
|
3bced80b23 | ||
|
|
f46f3e939e | ||
|
|
9d8e836fdd | ||
|
|
132d34db5c | ||
|
|
16edb7b35a | ||
|
|
044478a044 | ||
|
|
f6ea48d666 | ||
|
|
6ed3ed1617 | ||
|
|
9c9c401e66 | ||
|
|
72031edfbe | ||
|
|
41456aa2ab | ||
|
|
a70f29381a | ||
|
|
3b0ad73732 | ||
|
|
8c302648bb | ||
|
|
01b888c341 | ||
|
|
f8e87a8b6c | ||
|
|
01af83946c | ||
|
|
386a9ad0b0 | ||
|
|
c6c20d8f3c | ||
|
|
40b072711e | ||
|
|
2469964a44 | ||
|
|
40e0924768 | ||
|
|
e016440fb3 | ||
|
|
a37aaa5585 | ||
|
|
1ee551d53e | ||
|
|
2341eed796 | ||
|
|
49cfd79505 | ||
|
|
9ac0d1a1ce | ||
|
|
6046dfabe9 | ||
|
|
c2269bf777 | ||
|
|
b81a958021 | ||
|
|
9e37a5643c | ||
|
|
50a213c2e1 | ||
|
|
c6e9fbd501 | ||
|
|
720b5aa7c1 | ||
|
|
7be7640d3f | ||
|
|
e4d97278ee | ||
|
|
793ad82d42 | ||
|
|
2b4d846f0e | ||
|
|
379eadab4f | ||
|
|
53a41fcffe | ||
|
|
5e597e1f09 | ||
|
|
c9879f863b | ||
|
|
e1b260d0ec | ||
|
|
480b05063d | ||
|
|
8477f7b472 | ||
|
|
91ae4eab06 | ||
|
|
f6d27516cb | ||
|
|
7b86681cb2 | ||
|
|
1de815441a | ||
|
|
d6426dc1b6 | ||
|
|
44a36025b5 | ||
|
|
00017ab653 | ||
|
|
411adc861e | ||
|
|
c5b3939ddb | ||
|
|
a9e3ea56ec | ||
|
|
6a0f17a983 | ||
|
|
28e6f457b1 | ||
|
|
fdf46054e2 | ||
|
|
47a3ee1ff1 | ||
|
|
a0b7b4b5c4 | ||
|
|
b9bc0a4047 | ||
|
|
e57abf92f3 | ||
|
|
b56a7bc139 | ||
|
|
c401780c15 | ||
|
|
df96a1daac | ||
|
|
0a2200c2c8 | ||
|
|
61b8d04418 | ||
|
|
fd7cc83537 | ||
|
|
f24843fbb1 | ||
|
|
6c57bc9438 | ||
|
|
ae5f72cf4f | ||
|
|
3e65b6f3a6 | ||
|
|
a4a53d5299 | ||
|
|
4223cac7a5 | ||
|
|
0073a09da6 | ||
|
|
e612927c5d | ||
|
|
aff951440c | ||
|
|
ef63e01632 | ||
|
|
c4cf0f12c9 | ||
|
|
68635be8a2 | ||
|
|
776df7505e | ||
|
|
d6fdc7cb67 | ||
|
|
91c10b3ac6 | ||
|
|
82ace72527 | ||
|
|
ea87c78d34 | ||
|
|
585b8ece58 | ||
|
|
a2927a6586 | ||
|
|
943c8a1ab3 | ||
|
|
3400f5641e | ||
|
|
5068648960 | ||
|
|
5be558ea68 | ||
|
|
fc25bba514 | ||
|
|
20b326415a | ||
|
|
1bf5064039 | ||
|
|
edf0c02bc8 | ||
|
|
1d42907114 | ||
|
|
070d832580 | ||
|
|
0dfec83b0f | ||
|
|
c84155cbd4 | ||
|
|
eb5ddf270f | ||
|
|
fb093253c6 | ||
|
|
1864be5c55 | ||
|
|
7138d44083 | ||
|
|
04daff0608 | ||
|
|
801250a9e0 | ||
|
|
20319b5426 | ||
|
|
9cca34bba5 | ||
|
|
530981119e | ||
|
|
6d0327d057 | ||
|
|
580ce5a9e9 | ||
|
|
6f327c950d | ||
|
|
92ad843ff2 | ||
|
|
a8059c6bff | ||
|
|
4b468a25fe | ||
|
|
1e135b649c | ||
|
|
40d32f2d0c | ||
|
|
c9ec087cd8 | ||
|
|
84d79e1479 | ||
|
|
83af248068 | ||
|
|
4f25edb1a1 | ||
|
|
ded1634b7d | ||
|
|
635c73ffc6 | ||
|
|
fcc1fe73be | ||
|
|
fa278d50f7 | ||
|
|
2f02be4c64 | ||
|
|
7add1c116c | ||
|
|
124a0e90e1 | ||
|
|
1716cdf51c | ||
|
|
3fdcffb314 | ||
|
|
f033aae25c | ||
|
|
c42d942460 | ||
|
|
0ba8201797 | ||
|
|
87252ab053 | ||
|
|
53eec521dc | ||
|
|
238570a7b9 | ||
|
|
043ae48806 | ||
|
|
fb88f2e6ab | ||
|
|
5db867cd1b | ||
|
|
ec00c160c6 | ||
|
|
616eabc613 | ||
|
|
89b32e02c5 | ||
|
|
e985588c6c | ||
|
|
7ec3a1a9a2 | ||
|
|
19fa86b276 | ||
|
|
c4657991c8 | ||
|
|
484aebdb16 | ||
|
|
9c15cd5c8f | ||
|
|
8302d22622 | ||
|
|
034cde9289 | ||
|
|
02455d8485 | ||
|
|
35f50a8965 | ||
|
|
e04efdbd94 | ||
|
|
57445eedb1 | ||
|
|
a501f10756 | ||
|
|
5d80d4788c | ||
|
|
0c02886005 | ||
|
|
24856f3050 | ||
|
|
8e6434068e | ||
|
|
800d2b14a5 | ||
|
|
3a861d2f84 | ||
|
|
4ba00f7440 | ||
|
|
40fc61da4f | ||
|
|
eb0f896d57 | ||
|
|
71bb89fac1 | ||
|
|
b89199db54 | ||
|
|
e39429c2e3 | ||
|
|
17de3d3236 | ||
|
|
3177f9967d | ||
|
|
81418d8ee5 | ||
|
|
a2e7d914a0 | ||
|
|
4bf38c0e29 | ||
|
|
0079cd4766 | ||
|
|
2c3b2b8c2d | ||
|
|
52fa58a3ce | ||
|
|
32a7e5ed82 | ||
|
|
097113f01e | ||
|
|
1d42e4743f | ||
|
|
5ecdea47db | ||
|
|
5b92b6355e | ||
|
|
5eb7206b2d | ||
|
|
0a65081db0 | ||
|
|
a59e72e7d8 | ||
|
|
fd358617f5 | ||
|
|
b26a351786 | ||
|
|
575b43d9a0 | ||
|
|
0a5542a698 | ||
|
|
518bd19e96 | ||
|
|
961e3ad7e2 | ||
|
|
7a49e9401f | ||
|
|
3701936129 | ||
|
|
c02686b56e | ||
|
|
9a7ff9d2b1 | ||
|
|
56f6d6849e | ||
|
|
cbc18ee5a4 | ||
|
|
14521cfc2d | ||
|
|
5e4807b7ac | ||
|
|
28d9bec0b4 | ||
|
|
05e50ea787 | ||
|
|
02afacf989 | ||
|
|
c7de4f66e7 | ||
|
|
c9b8c5079b | ||
|
|
eec5ae96e8 | ||
|
|
4b94eadf5e | ||
|
|
52a1886937 | ||
|
|
9767f51c3d | ||
|
|
6674b888cc | ||
|
|
a5e6bd3e8e | ||
|
|
b6c24932a7 | ||
|
|
d73d56c399 | ||
|
|
731e90f0d5 | ||
|
|
e0a6c2ef54 | ||
|
|
c5408e0561 | ||
|
|
c1a2df91ac | ||
|
|
da85c2412e | ||
|
|
d108f9b3e3 | ||
|
|
e3014a349c | ||
|
|
9d88ef069e | ||
|
|
155dff2813 | ||
|
|
38d4ea8514 | ||
|
|
6f24874eb8 | ||
|
|
2d20812652 | ||
|
|
5762fbb9a7 | ||
|
|
0e06da22df | ||
|
|
5833a9b347 | ||
|
|
0ef8d57881 | ||
|
|
fc64c33368 | ||
|
|
1b39be8a42 | ||
|
|
a1e19e2c41 | ||
|
|
b920db12c7 | ||
|
|
73b90eee3e | ||
|
|
4637a28bf6 | ||
|
|
d0638c1542 | ||
|
|
788d3125a3 | ||
|
|
3c4ffc3550 | ||
|
|
ada858f439 | ||
|
|
f2570945c6 | ||
|
|
8072f78058 | ||
|
|
8ae0ee5a67 | ||
|
|
a75d2b1c80 | ||
|
|
c48c2af7a1 | ||
|
|
490a14c5ef | ||
|
|
dcce6ef50b | ||
|
|
7cf0820d2b | ||
|
|
0bae3caaff | ||
|
|
bca0b256c9 | ||
|
|
a53d30c459 | ||
|
|
7a9f497aa7 | ||
|
|
f9f9bc3efb | ||
|
|
904990bf91 | ||
|
|
b2266ffca1 | ||
|
|
bb9a3d4b8e | ||
|
|
e565e19b42 | ||
|
|
41319c85c7 | ||
|
|
daf56804a5 | ||
|
|
6f7a43804d | ||
|
|
0ca76d36ef | ||
|
|
ec5789997a | ||
|
|
7a0d61bbb0 | ||
|
|
1c2461974d | ||
|
|
2a754744fe | ||
|
|
b413593c43 | ||
|
|
c73edd7e21 | ||
|
|
a34a69d8e4 | ||
|
|
020a9d33f6 | ||
|
|
19f6f89312 | ||
|
|
d56e05a11a | ||
|
|
c379a4e5a7 | ||
|
|
44c1efe4e4 | ||
|
|
ff0d675082 | ||
|
|
e1087b4145 | ||
|
|
323535584b | ||
|
|
852adbe514 | ||
|
|
4c78553d90 | ||
|
|
a31ae5297a | ||
|
|
e7792a0c65 | ||
|
|
3c32de1859 | ||
|
|
6a3fe3db92 | ||
|
|
ac048c154d | ||
|
|
3f51a8ffc2 | ||
|
|
2129b2b7a0 | ||
|
|
3734fc25a7 | ||
|
|
05ddc13054 | ||
|
|
716504b833 | ||
|
|
187861c3b2 | ||
|
|
0b075ac762 | ||
|
|
a6c889ed5e | ||
|
|
ca1533b0e4 | ||
|
|
3267596a30 | ||
|
|
5f29b93970 | ||
|
|
2a6a21c33a | ||
|
|
059af398eb | ||
|
|
6044e5961b |
25
.github/mergeable.yml
vendored
25
.github/mergeable.yml
vendored
@@ -1,26 +1,15 @@
|
||||
version: 2
|
||||
mergeable:
|
||||
- when: pull_request.*
|
||||
name: "Changelog check"
|
||||
name: "Conventional Commits"
|
||||
validate:
|
||||
- do: or
|
||||
validate:
|
||||
- do: description
|
||||
must_include:
|
||||
regex: "#skip-changelog"
|
||||
- do: and
|
||||
validate:
|
||||
- do: dependent
|
||||
changed:
|
||||
file: "src/**"
|
||||
required: ["CHANGELOG.md"]
|
||||
- do: dependent
|
||||
changed:
|
||||
file: "deltachat-ffi/src/**"
|
||||
required: ["CHANGELOG.md"]
|
||||
- do: title
|
||||
begins_with:
|
||||
match: ['feat', 'fix', 'api', 'refactor', 'perf', 'test', 'style', 'chore']
|
||||
|
||||
fail:
|
||||
- do: checks
|
||||
status: "action_required"
|
||||
payload:
|
||||
title: Changelog might need an update
|
||||
summary: "Check if CHANGELOG.md needs an update or add #skip-changelog to the PR description."
|
||||
title: PR title should follow conventional commits
|
||||
summary: "PR title should follow https://conventionalcommits.org. E.g. start with 'feat:' (for Features / Changes), 'fix:' (for Fixes), 'api:' (for API-Changes), 'api!:' (for breaking API-Changes) 'refactor:' (for Refactor), 'perf:' (for Performance), 'test:' (for Tests), 'style:' (for Styling), 'chore:' (for Miscellaneous Tasks)"
|
||||
|
||||
205
.github/workflows/ci.yml
vendored
205
.github/workflows/ci.yml
vendored
@@ -16,11 +16,11 @@ env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Rustfmt and Clippy
|
||||
lint_rust:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.68.0
|
||||
RUSTUP_TOOLCHAIN: 1.68.2
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install rustfmt and clippy
|
||||
@@ -31,6 +31,8 @@ jobs:
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Run clippy
|
||||
run: scripts/clippy.sh
|
||||
- name: Check
|
||||
run: cargo check --workspace --all-targets --all-features
|
||||
|
||||
cargo_deny:
|
||||
name: cargo deny
|
||||
@@ -64,31 +66,28 @@ jobs:
|
||||
- name: Rustdoc
|
||||
run: cargo doc --document-private-items --no-deps
|
||||
|
||||
build_and_test:
|
||||
name: Build and test
|
||||
rust_tests:
|
||||
name: Rust tests
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.68.0
|
||||
python: 3.9
|
||||
rust: 1.68.2
|
||||
- os: windows-latest
|
||||
rust: 1.68.0
|
||||
python: false # Python bindings compilation on Windows is not supported.
|
||||
rust: 1.68.2
|
||||
- os: macos-latest
|
||||
rust: 1.68.2
|
||||
|
||||
# Minimum Supported Rust Version = 1.64.0
|
||||
# Minimum Supported Rust Version = 1.65.0
|
||||
#
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.64.0
|
||||
python: 3.7
|
||||
rust: 1.65.0
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust ${{ matrix.rust }}
|
||||
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
|
||||
@@ -97,64 +96,176 @@ jobs:
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
|
||||
- name: Check
|
||||
run: cargo check --workspace --bins --examples --tests --benches
|
||||
|
||||
- name: Tests
|
||||
run: cargo test --workspace
|
||||
|
||||
- name: Test cargo vendor
|
||||
run: cargo vendor
|
||||
|
||||
c_library:
|
||||
name: Build C library
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
|
||||
- name: Build C library
|
||||
run: cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
- name: Upload C library
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug/libdeltachat.a
|
||||
retention-days: 1
|
||||
|
||||
rpc_server:
|
||||
name: Build deltachat-rpc-server
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
|
||||
- name: Build deltachat-rpc-server
|
||||
run: cargo build -p deltachat-rpc-server
|
||||
|
||||
- name: Upload deltachat-rpc-server
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: target/debug/deltachat-rpc-server
|
||||
retention-days: 1
|
||||
|
||||
python_lint:
|
||||
name: Python lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
|
||||
- name: Lint Python bindings
|
||||
working-directory: python
|
||||
run: tox -e lint
|
||||
|
||||
- name: Lint deltachat-rpc-client
|
||||
working-directory: deltachat-rpc-client
|
||||
run: tox -e lint
|
||||
|
||||
python_tests:
|
||||
name: Python tests
|
||||
needs: ["c_library", "python_lint"]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.11
|
||||
- os: macos-latest
|
||||
python: 3.11
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
python: pypy3.9
|
||||
- os: macos-latest
|
||||
python: pypy3.9
|
||||
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built. Test it with minimum supported Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.7
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download libdeltachat.a
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug
|
||||
|
||||
- name: Install python
|
||||
if: ${{ matrix.python }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
- name: Install tox
|
||||
if: ${{ matrix.python }}
|
||||
run: pip install tox
|
||||
|
||||
- name: Build C library
|
||||
if: ${{ matrix.python }}
|
||||
run: cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
- name: Run python tests
|
||||
if: ${{ matrix.python }}
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
DCC_RS_TARGET: debug
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e lint,mypy,doc,py3
|
||||
run: tox -e mypy,doc,py
|
||||
|
||||
- name: Build deltachat-rpc-server
|
||||
if: ${{ matrix.python }}
|
||||
run: cargo build -p deltachat-rpc-server
|
||||
aysnc_python_tests:
|
||||
name: Async Python tests
|
||||
needs: ["python_lint", "rpc_server"]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.11
|
||||
- os: macos-latest
|
||||
python: 3.11
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
python: pypy3.9
|
||||
- os: macos-latest
|
||||
python: pypy3.9
|
||||
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built. Test it with minimum supported Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.7
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
|
||||
- name: Download deltachat-rpc-server
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: target/debug
|
||||
|
||||
- name: Make deltachat-rpc-server executable
|
||||
run: chmod +x target/debug/deltachat-rpc-server
|
||||
|
||||
- name: Add deltachat-rpc-server to path
|
||||
if: ${{ matrix.python }}
|
||||
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
|
||||
|
||||
- name: Run deltachat-rpc-client tests
|
||||
if: ${{ matrix.python }}
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
working-directory: deltachat-rpc-client
|
||||
run: tox -e py3,lint
|
||||
|
||||
- name: Install pypy
|
||||
if: ${{ matrix.python }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "pypy${{ matrix.python }}"
|
||||
|
||||
- name: Run pypy tests
|
||||
if: ${{ matrix.python }}
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
DCC_RS_TARGET: debug
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e pypy3
|
||||
run: tox -e py
|
||||
|
||||
113
.github/workflows/deltachat-rpc-server.yml
vendored
113
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -8,35 +8,14 @@ concurrency:
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
# Build a version statically linked against musl libc
|
||||
# to avoid problems with glibc version incompatibility.
|
||||
build_static_linux:
|
||||
name: Build deltachat-rpc-server for Linux
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add x86_64-unknown-linux-musl
|
||||
|
||||
- name: Install musl-gcc
|
||||
run: sudo apt install musl-tools
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
RUSTFLAGS: "-C link-arg=-s"
|
||||
run: cargo build --release --target x86_64-unknown-linux-musl -p deltachat-rpc-server --features vendored
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64
|
||||
path: target/x86_64-unknown-linux-musl/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
build_linux:
|
||||
name: Cross-compile deltachat-rpc-server for aarch64 and armv7 Linux
|
||||
name: Cross-compile deltachat-rpc-server for x86_64, i686, aarch64 and armv7 Linux
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -44,6 +23,20 @@ jobs:
|
||||
- name: Build
|
||||
run: sh scripts/zig-rpc-server.sh
|
||||
|
||||
- name: Upload x86_64 binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64
|
||||
path: target/x86_64-unknown-linux-musl/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload i686 binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-i686
|
||||
path: target/i686-unknown-linux-musl/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload aarch64 binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -58,50 +51,6 @@ jobs:
|
||||
path: target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
build_android:
|
||||
name: Cross-compile deltachat-rpc-server for Android (armeabi-v7a, arm64-v8a, x86 and x86_64)
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r21d
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
run: sh scripts/android-rpc-server.sh
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-android-armv7
|
||||
path: target/armv7-linux-androideabi/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-android-aarch64
|
||||
path: target/aarch64-linux-android/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-android-i686
|
||||
path: target/i686-linux-android/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-android-x86_64
|
||||
path: target/x86_64-linux-android/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
build_windows:
|
||||
name: Build deltachat-rpc-server for Windows
|
||||
strategy:
|
||||
@@ -134,3 +83,31 @@ jobs:
|
||||
name: deltachat-rpc-server-${{ matrix.artifact }}
|
||||
path: target/${{ matrix.target}}/release/${{ matrix.path }}
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Upload binaries to the release
|
||||
needs: ["build_linux", "build_windows"]
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: Download built binaries
|
||||
uses: "actions/download-artifact@v3"
|
||||
|
||||
- name: Compose dist/ directory
|
||||
run: |
|
||||
mkdir dist
|
||||
for x in x86_64 i686 aarch64 armv7 win32.exe win64.exe; do
|
||||
mv "deltachat-rpc-server-$x"/* "dist/deltachat-rpc-server-$x"
|
||||
done
|
||||
|
||||
- name: List downloaded artifacts
|
||||
run: ls -l dist/
|
||||
|
||||
- name: Upload binaries to the GitHub release
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
run: |
|
||||
gh release upload ${{ github.ref_name }} \
|
||||
--repo ${{ github.repository }} \
|
||||
dist/*
|
||||
|
||||
151
CHANGELOG.md
151
CHANGELOG.md
@@ -3,11 +3,131 @@
|
||||
## Unreleased
|
||||
|
||||
### Changes
|
||||
- "full message view" not needed because of footers that go to contact status #4151
|
||||
- Pick up system's light/dark mode in generated message HTML #4150
|
||||
- Support non-persistent configuration with DELTACHAT_* env
|
||||
- Print deltachat-repl errors with causes. #4166
|
||||
- BREAKING: jsonrpc:
|
||||
- `get_chatlist_items_by_entries` now takes only chatids instead of `ChatListEntries`
|
||||
- `get_chatlist_entries` now returns `Vec<u32>` of chatids instead of `ChatListEntries`
|
||||
- JSON-RPC: add API to get reactions outside the message snapshot
|
||||
### Fixes
|
||||
- Make the bots automatically accept group chat contact requests. #4377
|
||||
|
||||
|
||||
## [1.114.0] - 2023-04-24
|
||||
|
||||
### Changes
|
||||
- JSON-RPC: Use long polling instead of server-sent notifications to retrieve events.
|
||||
This better corresponds to JSON-RPC 2.0 server-client distinction
|
||||
and is expected to simplify writing new bindings
|
||||
because dispatching events can be done on higher level.
|
||||
- JSON-RPC: TS: Client now has a mandatory argument whether you want to start listening for events.
|
||||
|
||||
### Fixes
|
||||
- JSON-RPC: do not print to stdout on failure to find an account.
|
||||
|
||||
|
||||
## [1.113.0] - 2023-04-18
|
||||
|
||||
### Added
|
||||
- New JSON-RPC API `can_send()`.
|
||||
- New `dc_get_next_msgs()` and `dc_wait_next_msgs()` C APIs.
|
||||
New `get_next_msgs()` and `wait_next_msgs()` JSON-RPC API.
|
||||
These APIs can be used by bots to get all unprocessed messages
|
||||
in the order of their arrival and wait for them without relying on events.
|
||||
- New Python bindings API `Account.wait_next_incoming_message()`.
|
||||
- New Python bindings APIs `Message.is_from_self()` and `Message.is_from_device()`.
|
||||
|
||||
### Changes
|
||||
- Increase MSRV to 1.65.0. #4236
|
||||
- Remove upper limit on the attachment size. #4253
|
||||
- Update rPGP to 0.10.1. #4236
|
||||
- Compress HTML emails stored in the `mime_headers` column of the database.
|
||||
- Strip BIDI characters in system messages, files, group names and contact names. #3479
|
||||
- Use release date instead of the provider database update date in `maybe_add_time_based_warnings()`.
|
||||
- Gracefully terminate `deltachat-rpc-server` on Ctrl+C (`SIGINT`), `SIGTERM` and EOF.
|
||||
- Async Python API `get_fresh_messages_in_arrival_order()` is deprecated
|
||||
in favor of `get_next_msgs()` and `wait_next_msgs()`.
|
||||
- Remove metadata from avatars and JPEG images before sending. #4037
|
||||
- Recode PNG and other supported image formats to JPEG if they are > 500K in size. #4037
|
||||
|
||||
### Fixes
|
||||
- Don't let blocking be bypassed using groups. #4316
|
||||
- Show a warning if quota list is empty. #4261
|
||||
- Do not reset status on other devices when sending signed reaction messages. #3692
|
||||
- Update `accounts.toml` atomically.
|
||||
- Fix python bindings README documentation on installing the bindings from source.
|
||||
- Remove confusing log line "ignoring unsolicited response Recent(…)". #3934
|
||||
|
||||
## [1.112.8] - 2023-04-20
|
||||
|
||||
### Changes
|
||||
- Add `get_http_response` JSON-RPC API.
|
||||
- Add C API to get HTTP responses.
|
||||
|
||||
## [1.112.7] - 2023-04-17
|
||||
|
||||
### Fixes
|
||||
|
||||
- Updated `async-imap` to v0.8.0 to fix erroneous EOF detection in long IMAP responses.
|
||||
|
||||
## [1.112.6] - 2023-04-04
|
||||
|
||||
### Changes
|
||||
|
||||
- Add a device message after backup transfer #4301
|
||||
|
||||
### Fixed
|
||||
|
||||
- Updated `iroh` from 0.4.0 to 0.4.1 to fix transfer of large accounts with many blob files.
|
||||
|
||||
## [1.112.5] - 2023-04-02
|
||||
|
||||
### Fixes
|
||||
|
||||
- Run SQL database migrations after receiving a backup from the network. #4287
|
||||
|
||||
## [1.112.4] - 2023-03-31
|
||||
|
||||
### Fixes
|
||||
- Fix call to `auditwheel` in `scripts/run_all.sh`.
|
||||
|
||||
## [1.112.3] - 2023-03-30
|
||||
|
||||
### Fixes
|
||||
- `transfer::get_backup` now frees ongoing process when cancelled. #4249
|
||||
|
||||
## [1.112.2] - 2023-03-30
|
||||
|
||||
### Changes
|
||||
- Update iroh, remove `default-net` from `[patch.crates-io]` section.
|
||||
- transfer backup: Connect to multiple provider addresses concurrently. This should speed up connection time significantly on the getter side. #4240
|
||||
- Make sure BackupProvider is cancelled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242
|
||||
|
||||
### Fixes
|
||||
- Do not return media from trashed messages in the "All media" view. #4247
|
||||
|
||||
## [1.112.1] - 2023-03-27
|
||||
|
||||
### Changes
|
||||
- Add support for `--version` argument to `deltachat-rpc-server`. #4224
|
||||
It can be used to check the installed version without starting the server.
|
||||
|
||||
### Fixes
|
||||
- deltachat-rpc-client: fix bug in `Chat.send_message()`: invalid `MessageData` field `quotedMsg` instead of `quotedMsgId`
|
||||
- `receive_imf`: Mark special messages as seen. Exactly: delivery reports, webxdc status updates. #4230
|
||||
|
||||
|
||||
## [1.112.0] - 2023-03-23
|
||||
|
||||
### Changes
|
||||
- Increase MSRV to 1.64. #4167
|
||||
- Core takes care of stopping and re-starting IO itself where needed,
|
||||
e.g. during backup creation.
|
||||
It is no longer needed to call `dc_stop_io()`.
|
||||
`dc_start_io()` can now be called at any time without harm. #4138
|
||||
- Pick up system's light/dark mode in generated message HTML. #4150
|
||||
- More accurate `maybe_add_bcc_self` device message text. #4175
|
||||
- "Full message view" not needed because of footers that go to contact status. #4151
|
||||
- Support non-persistent configuration with `DELTACHAT_*` env. #4154
|
||||
- Print deltachat-repl errors with causes. #4166
|
||||
|
||||
### Fixes
|
||||
- Fix segmentation fault if `dc_context_unref()` is called during
|
||||
@@ -16,9 +136,15 @@
|
||||
during handling the JSON-RPC request. #4153
|
||||
- Delete expired messages using multiple SQL requests. #4158
|
||||
- Do not emit "Failed to run incremental vacuum" warnings on success. #4160
|
||||
- Ability to send backup over network and QR code to setup second device #4007
|
||||
- Disable buffering during STARTTLS setup. #4190
|
||||
- Add `DC_EVENT_IMAP_INBOX_IDLE` event to wait until the account
|
||||
is ready for testing.
|
||||
It is used to fix race condition between fetching
|
||||
existing messages and starting the test. #4208
|
||||
|
||||
|
||||
## 1.111.0
|
||||
## [1.111.0] - 2023-03-05
|
||||
|
||||
### Changes
|
||||
- Make smeared timestamp generation non-async. #4075
|
||||
@@ -147,7 +273,6 @@
|
||||
- Do not treat invalid email addresses as an exception #3942
|
||||
- Add timeouts to HTTP requests #3948
|
||||
|
||||
|
||||
## 1.105.0
|
||||
|
||||
### Changes
|
||||
@@ -233,7 +358,6 @@
|
||||
- Disable read timeout during IMAP IDLE #3826
|
||||
- Bots automatically accept mailing lists #3831
|
||||
|
||||
|
||||
## 1.102.0
|
||||
|
||||
### Changes
|
||||
@@ -2307,3 +2431,16 @@
|
||||
For a full list of changes, please see our closed Pull Requests:
|
||||
|
||||
https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
|
||||
[1.111.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.110.0...v1.111.0
|
||||
[1.112.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.111.0...v1.112.0
|
||||
[1.112.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.0...v1.112.1
|
||||
[1.112.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.1...v1.112.2
|
||||
[1.112.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.2...v1.112.3
|
||||
[1.112.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.3...v1.112.4
|
||||
[1.112.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.4...v1.112.5
|
||||
[1.112.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.5...v1.112.6
|
||||
[1.112.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.6...v1.112.7
|
||||
[1.112.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.7...v1.112.8
|
||||
[1.113.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.8...v1.113.0
|
||||
[1.114.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.113.0...v1.114.0
|
||||
|
||||
2182
Cargo.lock
generated
2182
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
38
Cargo.toml
38
Cargo.toml
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.111.0"
|
||||
version = "1.114.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.64"
|
||||
rust-version = "1.65"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
@@ -24,6 +24,10 @@ lto = true
|
||||
panic = 'abort'
|
||||
opt-level = "z"
|
||||
|
||||
[patch.crates-io]
|
||||
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
|
||||
quinn-proto = { git = "https://github.com/quinn-rs/quinn", branch="main" }
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
format-flowed = { path = "./format-flowed" }
|
||||
@@ -31,41 +35,43 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
|
||||
anyhow = "1"
|
||||
async-channel = "1.8.0"
|
||||
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
|
||||
async-imap = { version = "0.8.0", 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.9", default-features = false, features = ["deflate"] }
|
||||
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||
backtrace = "0.3"
|
||||
base64 = "0.21"
|
||||
bitflags = "1.3"
|
||||
brotli = "3.3"
|
||||
chrono = { version = "0.4", 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.8"
|
||||
futures = "0.3"
|
||||
futures-lite = "1.12.0"
|
||||
futures-lite = "1.13.0"
|
||||
hex = "0.4.0"
|
||||
humansize = "2"
|
||||
image = { version = "0.24.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
image = { version = "0.24.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh = { version = "0.4.1", default-features = false }
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
mailparse = "0.14"
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.15"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.17.0"
|
||||
percent-encoding = "2.2"
|
||||
parking_lot = "0.12"
|
||||
pgp = { version = "0.9", default-features = false }
|
||||
pgp = { version = "0.10", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.27"
|
||||
quick-xml = "0.28"
|
||||
rand = "0.8"
|
||||
regex = "1.7"
|
||||
reqwest = { version = "0.11.14", features = ["json"] }
|
||||
rusqlite = { version = "0.28", features = ["sqlcipher"] }
|
||||
reqwest = { version = "0.11.17", features = ["json"] }
|
||||
rusqlite = { version = "0.29", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
sanitize-filename = "0.4"
|
||||
serde_json = "1.0"
|
||||
@@ -78,10 +84,11 @@ strum_macros = "0.24"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.0"
|
||||
thiserror = "1"
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-stream = { version = "0.1.11", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-stream = { version = "0.1.14", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = "0.7.8"
|
||||
toml = "0.7"
|
||||
trust-dns-resolver = "0.22"
|
||||
url = "2"
|
||||
@@ -90,11 +97,12 @@ uuid = { version = "1", features = ["serde", "v4"] }
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
criterion = { version = "0.4.0", features = ["async_tokio"] }
|
||||
futures-lite = "1.12"
|
||||
futures-lite = "1.13"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
testdir = "0.7.3"
|
||||
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||
|
||||
[workspace]
|
||||
|
||||
75
cliff.toml
Normal file
75
cliff.toml
Normal file
@@ -0,0 +1,75 @@
|
||||
# configuration file for git-cliff
|
||||
# see https://github.com/orhun/git-cliff#configuration-file
|
||||
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# process each line of a commit as an individual commit
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/deltachat/deltachat-core-rust/PR/${2}))"}, # replace pull request / issue numbers
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features / Changes"},
|
||||
{ message = "^fix", group = "Fixes"},
|
||||
{ message = "^api", group = "API-Changes" },
|
||||
{ message = "^refactor", group = "Refactor"},
|
||||
{ message = "^perf", group = "Performance"},
|
||||
{ message = "^test", group = "Tests"},
|
||||
{ message = "^style", group = "Styling"},
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true},
|
||||
{ message = "^chore", group = "Miscellaneous Tasks"},
|
||||
# { body = ".*security", group = "Security"},
|
||||
]
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = true
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = true
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "v[0-9]*"
|
||||
# regex for skipping tags
|
||||
#skip_tags = "v0.1.0-beta.1"
|
||||
# regex for ignoring tags
|
||||
ignore_tags = ""
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "oldest"
|
||||
# limit the number of commits included in the changelog.
|
||||
# limit_commits = 42
|
||||
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits %}
|
||||
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
|
||||
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
|
||||
{% raw %} {% endraw %}- {{ footer.value }}\
|
||||
{% endif %}{% endfor %}\
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.111.0"
|
||||
version = "1.114.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
@@ -17,13 +17,13 @@ crate-type = ["cdylib", "staticlib"]
|
||||
deltachat = { path = "../", default-features = false }
|
||||
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
|
||||
libc = "0.2"
|
||||
human-panic = "1"
|
||||
human-panic = { version = "1", default-features = false }
|
||||
num-traits = "0.2"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
rand = "0.7"
|
||||
rand = "0.8"
|
||||
once_cell = "1.17.0"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -24,6 +24,8 @@ typedef struct _dc_provider dc_provider_t;
|
||||
typedef struct _dc_event dc_event_t;
|
||||
typedef struct _dc_event_emitter dc_event_emitter_t;
|
||||
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
|
||||
typedef struct _dc_backup_provider dc_backup_provider_t;
|
||||
typedef struct _dc_http_response dc_http_response_t;
|
||||
|
||||
// Alias for backwards compatibility, use dc_event_emitter_t instead.
|
||||
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
|
||||
@@ -71,7 +73,6 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
|
||||
*
|
||||
* The example above uses "pthreads",
|
||||
* however, you can also use anything else for thread handling.
|
||||
* All deltachat-core functions, unless stated otherwise, are thread-safe.
|
||||
*
|
||||
* Now you can **configure the context:**
|
||||
*
|
||||
@@ -141,6 +142,72 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
|
||||
* ~~~
|
||||
*
|
||||
*
|
||||
* ## Thread safety
|
||||
*
|
||||
* All deltachat-core functions, unless stated otherwise, are thread-safe.
|
||||
* In particular, it is safe to pass the same dc_context_t pointer
|
||||
* to multiple functions running concurrently in different threads.
|
||||
*
|
||||
* All the functions are guaranteed not to use the reference passed to them
|
||||
* after returning. If the function spawns a long-running process,
|
||||
* such as dc_configure() or dc_imex(), it will ensure that the objects
|
||||
* passed to them are not deallocated as long as they are needed.
|
||||
* For example, it is safe to call dc_imex(context, ...) and
|
||||
* call dc_context_unref(context) immediately after return from dc_imex().
|
||||
* It is however **not safe** to call dc_context_unref(context) concurrently
|
||||
* until dc_imex() returns, because dc_imex() may have not increased
|
||||
* the reference count of dc_context_t yet.
|
||||
*
|
||||
* This means that the context may be still in use after
|
||||
* dc_context_unref() call.
|
||||
* For example, it is possible to start the import/export process,
|
||||
* call dc_context_unref(context) immediately after
|
||||
* and observe #DC_EVENT_IMEX_PROGRESS events via the event emitter.
|
||||
* Once dc_get_next_event() returns NULL,
|
||||
* it is safe to terminate the application.
|
||||
*
|
||||
* It is recommended to create dc_context_t in the main thread
|
||||
* and only call dc_context_unref() once other threads that may use it,
|
||||
* such as the event loop thread, are terminated.
|
||||
* Common mistake is to use dc_context_unref() as a way
|
||||
* to cause dc_get_next_event() return NULL and terminate event loop this way.
|
||||
* If event loop thread is inside a function taking dc_context_t
|
||||
* as an argument at the moment dc_context_unref() is called on the main thread,
|
||||
* the behavior is undefined.
|
||||
*
|
||||
* Recommended way to safely terminate event loop
|
||||
* and shutdown the application is
|
||||
* to use a boolean variable
|
||||
* indicating that the event loop should stop
|
||||
* and check it in the event loop thread
|
||||
* every time before calling dc_get_next_event().
|
||||
* To terminate the event loop, main thread should:
|
||||
* 1. Notify background threads,
|
||||
* such as event loop (blocking in dc_get_next_event())
|
||||
* and message processing loop (blocking in dc_wait_next_msgs()),
|
||||
* that they should terminate by atomically setting the
|
||||
* boolean flag in the memory
|
||||
* shared between the main thread and background loop threads.
|
||||
* 2. Call dc_stop_io() or dc_accounts_stop_io(), depending
|
||||
* on whether a single account or account manager is used.
|
||||
* Stopping I/O is guaranteed to emit at least one event
|
||||
* and interrupt the event loop even if it was blocked on dc_get_next_event().
|
||||
* Stopping I/O is guaranteed to interrupt a single dc_wait_next_msgs().
|
||||
* 3. Wait until the event loop thread notices the flag,
|
||||
* exits the event loop and terminates.
|
||||
* 4. Call dc_context_unref() or dc_accounts_unref().
|
||||
* 5. Keep calling dc_get_next_event() in a loop until it returns NULL,
|
||||
* indicating that the contexts are deallocated.
|
||||
* 6. Terminate the application.
|
||||
*
|
||||
* When using C API via FFI in runtimes that use automatic memory management,
|
||||
* such as CPython, JVM or Node.js, take care to ensure the correct
|
||||
* shutdown order and avoid calling dc_context_unref() or dc_accounts_unref()
|
||||
* on the objects still in use in other threads,
|
||||
* e.g. by keeping a reference to the wrapper object.
|
||||
* The details depend on the runtime being used.
|
||||
*
|
||||
*
|
||||
* ## Class reference
|
||||
*
|
||||
* For a class reference, see the "Classes" link atop.
|
||||
@@ -396,6 +463,16 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* Prevents adding the "Device messages" and "Saved messages" chats,
|
||||
* adds Auto-Submitted header to outgoing messages
|
||||
* and accepts contact requests automatically (calling dc_accept_chat() is not needed for bots).
|
||||
* - `last_msg_id` = database ID of the last message processed by the bot.
|
||||
* This ID and IDs below it are guaranteed not to be returned
|
||||
* by dc_get_next_msgs() and dc_wait_next_msgs().
|
||||
* The value is updated automatically
|
||||
* when dc_markseen_msgs() is called,
|
||||
* but the bot can also set it manually if it processed
|
||||
* the message but does not want to mark it as seen.
|
||||
* For most bots calling `dc_markseen_msgs()` is the
|
||||
* recommended way to update this value
|
||||
* even for self-sent messages.
|
||||
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
|
||||
* 0=do not fetch existing messages on configure.
|
||||
* In both cases, existing recipients are added to the contact database.
|
||||
@@ -1282,6 +1359,56 @@ int dc_estimate_deletion_cnt (dc_context_t* context, int from_ser
|
||||
dc_array_t* dc_get_fresh_msgs (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Returns the message IDs of all messages of any chat
|
||||
* with a database ID higher than `last_msg_id` config value.
|
||||
*
|
||||
* This function is intended for use by bots.
|
||||
* Self-sent messages, device messages,
|
||||
* messages from contact requests
|
||||
* and muted chats are included,
|
||||
* but messages from explicitly blocked contacts
|
||||
* and chats are ignored.
|
||||
*
|
||||
* This function may be called as a part of event loop
|
||||
* triggered by DC_EVENT_INCOMING_MSG if you are only interested
|
||||
* in the incoming messages.
|
||||
* Otherwise use a separate message processing loop
|
||||
* calling dc_wait_next_msgs() in a separate thread.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @return An array of message IDs, must be dc_array_unref()'d when no longer used.
|
||||
* On errors, the list is empty. NULL is never returned.
|
||||
*/
|
||||
dc_array_t* dc_get_next_msgs (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Waits for notification of new messages
|
||||
* and returns an array of new message IDs.
|
||||
* See the documentation for dc_get_next_msgs()
|
||||
* for the details of return value.
|
||||
*
|
||||
* This function waits for internal notification of
|
||||
* a new message in the database and returns afterwards.
|
||||
* Notification is also sent when I/O is started
|
||||
* to allow processing new messages
|
||||
* and when I/O is stopped using dc_stop_io() or dc_accounts_stop_io()
|
||||
* to allow for manual interruption of the message processing loop.
|
||||
* The function may return an empty array if there are
|
||||
* no messages after notification,
|
||||
* which may happen on start or if the message is quickly deleted
|
||||
* after adding it to the database.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @return An array of message IDs, must be dc_array_unref()'d when no longer used.
|
||||
* On errors, the list is empty. NULL is never returned.
|
||||
*/
|
||||
dc_array_t* dc_wait_next_msgs (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Mark all messages in a chat as _noticed_.
|
||||
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
|
||||
@@ -1881,6 +2008,11 @@ int dc_resend_msgs (dc_context_t* context, const uint3
|
||||
* Moreover, timer is started for incoming ephemeral messages.
|
||||
* This also happens for contact requests chats.
|
||||
*
|
||||
* This function updates last_msg_id configuration value
|
||||
* to the maximum of the current value and IDs passed to this function.
|
||||
* Bots which mark messages as seen can rely on this side effect
|
||||
* to avoid updating last_msg_id value manually.
|
||||
*
|
||||
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -2100,8 +2232,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
|
||||
|
||||
/**
|
||||
* Import/export things.
|
||||
* During backup import/export IO must not be started,
|
||||
* if needed stop IO using dc_accounts_stop_io() or dc_stop_io() first.
|
||||
*
|
||||
* What to do is defined by the _what_ parameter which may be one of the following:
|
||||
*
|
||||
* - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1`
|
||||
@@ -2295,6 +2426,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_FPR_MISMATCH 220 // id=contact
|
||||
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
|
||||
#define DC_QR_ACCOUNT 250 // text1=domain
|
||||
#define DC_QR_BACKUP 251
|
||||
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
|
||||
#define DC_QR_ADDR 320 // id=contact
|
||||
#define DC_QR_TEXT 330 // text1=text
|
||||
@@ -2320,7 +2452,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* ask whether to verify the contact;
|
||||
* if so, start the protocol with dc_join_securejoin().
|
||||
*
|
||||
* - DC_QR_ASK_VERIFYGROUP withdc_lot_t::text1=Group name:
|
||||
* - DC_QR_ASK_VERIFYGROUP with dc_lot_t::text1=Group name:
|
||||
* ask whether to join the group;
|
||||
* if so, start the protocol with dc_join_securejoin().
|
||||
*
|
||||
@@ -2340,6 +2472,10 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* ask the user if they want to create an account on the given domain,
|
||||
* if so, call dc_set_config_from_qr() and then dc_configure().
|
||||
*
|
||||
* - DC_QR_BACKUP:
|
||||
* ask the user if they want to set up a new device.
|
||||
* If so, pass the qr-code to dc_receive_backup().
|
||||
*
|
||||
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
|
||||
* ask the user if they want to use the given service for video chats;
|
||||
* if so, call dc_set_config_from_qr().
|
||||
@@ -2630,6 +2766,123 @@ char* dc_get_last_error (dc_context_t* context);
|
||||
void dc_str_unref (char* str);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_backup_provider_t
|
||||
*
|
||||
* Set up another device.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates an object for sending a backup to another device.
|
||||
*
|
||||
* The backup is sent to through a peer-to-peer channel which is bootstrapped
|
||||
* by a QR-code. The backup contains the entire state of the account
|
||||
* including credentials. This can be used to setup a new device.
|
||||
*
|
||||
* This is a blocking call as some preparations are made like e.g. exporting
|
||||
* the database. Once this function returns, the backup is being offered to
|
||||
* remote devices. To wait until one device received the backup, use
|
||||
* dc_backup_provider_wait(). Alternatively abort the operation using
|
||||
* dc_stop_ongoing_process().
|
||||
*
|
||||
* During execution of the job #DC_EVENT_IMEX_PROGRESS is sent out to indicate
|
||||
* state and progress.
|
||||
*
|
||||
* @memberof dc_backup_provider_t
|
||||
* @param context The context.
|
||||
* @return Opaque object for sending the backup.
|
||||
* On errors, NULL is returned and dc_get_last_error() returns an error that
|
||||
* should be shown to the user.
|
||||
*/
|
||||
dc_backup_provider_t* dc_backup_provider_new (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Returns the QR code text that will offer the backup to other devices.
|
||||
*
|
||||
* The QR code contains a ticket which will validate the backup and provide
|
||||
* authentication for both the provider and the recipient.
|
||||
*
|
||||
* The scanning device should call the scanned text to dc_check_qr(). If
|
||||
* dc_check_qr() returns DC_QR_BACKUP, the backup transfer can be started using
|
||||
* dc_get_backup().
|
||||
*
|
||||
* @memberof dc_backup_provider_t
|
||||
* @param backup_provider The backup provider object as created by
|
||||
* dc_backup_provider_new().
|
||||
* @return The text that should be put in the QR code.
|
||||
* On errors an empty string is returned, NULL is never returned.
|
||||
* the returned string must be released using dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_backup_provider_get_qr (const dc_backup_provider_t* backup_provider);
|
||||
|
||||
|
||||
/**
|
||||
* Returns the QR code SVG image that will offer the backup to other devices.
|
||||
*
|
||||
* This works like dc_backup_provider_qr() but returns the text of a rendered
|
||||
* SVG image containing the QR code.
|
||||
*
|
||||
* @memberof dc_backup_provider_t
|
||||
* @param backup_provider The backup provider object as created by
|
||||
* dc_backup_provider_new().
|
||||
* @return The QR code rendered as SVG.
|
||||
* On errors an empty string is returned, NULL is never returned.
|
||||
* the returned string must be released using dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_backup_provider_get_qr_svg (const dc_backup_provider_t* backup_provider);
|
||||
|
||||
/**
|
||||
* Waits for the sending to finish.
|
||||
*
|
||||
* This is a blocking call and should only be called once.
|
||||
*
|
||||
* @memberof dc_backup_provider_t
|
||||
* @param backup_provider The backup provider object as created by
|
||||
* dc_backup_provider_new(). If NULL is given nothing is done.
|
||||
*/
|
||||
void dc_backup_provider_wait (dc_backup_provider_t* backup_provider);
|
||||
|
||||
/**
|
||||
* Frees a dc_backup_provider_t object.
|
||||
*
|
||||
* If the provider has not yet finished, as indicated by
|
||||
* dc_backup_provider_wait() or the #DC_EVENT_IMEX_PROGRESS event with value
|
||||
* of 0 (failed) or 1000 (succeeded), this will also abort any in-progress
|
||||
* transfer. If this aborts the provider a #DC_EVENT_IMEX_PROGRESS event with
|
||||
* value 0 (failed) will be emitted.
|
||||
*
|
||||
* @memberof dc_backup_provider_t
|
||||
* @param backup_provider The backup provider object as created by
|
||||
* dc_backup_provider_new().
|
||||
*/
|
||||
void dc_backup_provider_unref (dc_backup_provider_t* backup_provider);
|
||||
|
||||
/**
|
||||
* Gets a backup offered by a dc_backup_provider_t object on another device.
|
||||
*
|
||||
* This function is called on a device that scanned the QR code offered by
|
||||
* dc_backup_sender_qr() or dc_backup_sender_qr_svg(). Typically this is a
|
||||
* different device than that which provides the backup.
|
||||
*
|
||||
* This call will block while the backup is being transferred and only
|
||||
* complete on success or failure. Use dc_stop_ongoing_process() to abort it
|
||||
* early.
|
||||
*
|
||||
* During execution of the job #DC_EVENT_IMEX_PROGRESS is sent out to indicate
|
||||
* state and progress. The process is finished when the event emits either 0
|
||||
* or 1000, 0 means it failed and 1000 means it succeeded. These events are
|
||||
* for showing progress and informational only, success and failure is also
|
||||
* shown in the return code of this function.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context.
|
||||
* @param qr The qr code text, dc_check_qr() must have returned DC_QR_BACKUP
|
||||
* on this text.
|
||||
* @return 0=failure, 1=success.
|
||||
*/
|
||||
int dc_receive_backup (dc_context_t* context, const char* qr);
|
||||
|
||||
/**
|
||||
* @class dc_accounts_t
|
||||
*
|
||||
@@ -4875,6 +5128,72 @@ int dc_provider_get_status (const dc_provider_t* prov
|
||||
void dc_provider_unref (dc_provider_t* provider);
|
||||
|
||||
|
||||
/**
|
||||
* Return an HTTP(S) GET response.
|
||||
* This function can be used to download remote content for HTML emails.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object to take proxy settings from.
|
||||
* @param url HTTP or HTTPS URL.
|
||||
* @return The response must be released using dc_http_response_unref() after usage.
|
||||
* NULL is returned on errors.
|
||||
*/
|
||||
dc_http_response_t* dc_get_http_response (const dc_context_t* context, const char* url);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_http_response_t
|
||||
*
|
||||
* An object containing an HTTP(S) GET response.
|
||||
* Created by dc_get_http_response().
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Returns HTTP response MIME type as a string, e.g. "text/plain" or "text/html".
|
||||
*
|
||||
* @memberof dc_http_response_t
|
||||
* @param response HTTP response as returned by dc_get_http_response().
|
||||
* @return The string which must be released using dc_str_unref() after usage. May be NULL.
|
||||
*/
|
||||
char* dc_http_response_get_mimetype (const dc_http_response_t* response);
|
||||
|
||||
/**
|
||||
* Returns HTTP response encoding, e.g. "utf-8".
|
||||
*
|
||||
* @memberof dc_http_response_t
|
||||
* @param response HTTP response as returned by dc_get_http_response().
|
||||
* @return The string which must be released using dc_str_unref() after usage. May be NULL.
|
||||
*/
|
||||
char* dc_http_response_get_encoding (const dc_http_response_t* response);
|
||||
|
||||
/**
|
||||
* Returns HTTP response contents.
|
||||
*
|
||||
* @memberof dc_http_response_t
|
||||
* @param response HTTP response as returned by dc_get_http_response().
|
||||
* @return The blob which must be released using dc_str_unref() after usage. NULL is never returned.
|
||||
*/
|
||||
uint8_t* dc_http_response_get_blob (const dc_http_response_t* response);
|
||||
|
||||
/**
|
||||
* Returns HTTP response content size.
|
||||
*
|
||||
* @memberof dc_http_response_t
|
||||
* @param response HTTP response as returned by dc_get_http_response().
|
||||
* @return The blob size.
|
||||
*/
|
||||
size_t dc_http_response_get_size (const dc_http_response_t* response);
|
||||
|
||||
/**
|
||||
* Free an HTTP response object.
|
||||
*
|
||||
* @memberof dc_http_response_t
|
||||
* @param response HTTP response as returned by dc_get_http_response().
|
||||
*/
|
||||
void dc_http_response_unref (const dc_http_response_t* response);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_lot_t
|
||||
*
|
||||
@@ -5352,7 +5671,6 @@ void dc_reactions_unref (dc_reactions_t* reactions);
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_jsonrpc_instance_t
|
||||
*
|
||||
@@ -5589,6 +5907,14 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
|
||||
|
||||
/**
|
||||
* Emitted before going into IDLE on the Inbox folder.
|
||||
*
|
||||
* @param data1 0
|
||||
* @param data2 0
|
||||
*/
|
||||
#define DC_EVENT_IMAP_INBOX_IDLE 106
|
||||
|
||||
/**
|
||||
* Emitted when a new blob file was successfully written
|
||||
*
|
||||
@@ -6874,6 +7200,16 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161
|
||||
|
||||
/// "Scan to set up second device for %1$s"
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the account.
|
||||
#define DC_STR_BACKUP_TRANSFER_QR 162
|
||||
|
||||
/// "Account transferred to your second device."
|
||||
///
|
||||
/// Used as a device message after a successful backup transfer.
|
||||
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -28,9 +28,11 @@ use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, ContactId, Origin};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::imex::BackupProvider;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use deltachat::net::read_url_blob;
|
||||
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;
|
||||
@@ -159,7 +161,8 @@ pub unsafe extern "C" fn dc_context_open(
|
||||
let ctx = &*context;
|
||||
let passphrase = to_string_lossy(passphrase);
|
||||
block_on(ctx.open(passphrase))
|
||||
.log_err(ctx, "dc_context_open() failed")
|
||||
.context("dc_context_open() failed")
|
||||
.log_err(ctx)
|
||||
.map(|b| b as libc::c_int)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
@@ -215,16 +218,18 @@ pub unsafe extern "C" fn dc_set_config(
|
||||
if key.starts_with("ui.") {
|
||||
ctx.set_ui_config(&key, value.as_deref())
|
||||
.await
|
||||
.with_context(|| format!("Can't set {key} to {value:?}"))
|
||||
.log_err(ctx, "dc_set_config() failed")
|
||||
.with_context(|| format!("dc_set_config failed: Can't set {key} to {value:?}"))
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
} else {
|
||||
match config::Config::from_str(&key) {
|
||||
Ok(key) => ctx
|
||||
.set_config(key, value.as_deref())
|
||||
.await
|
||||
.with_context(|| format!("Can't set {key} to {value:?}"))
|
||||
.log_err(ctx, "dc_set_config() failed")
|
||||
.with_context(|| {
|
||||
format!("dc_set_config() failed: Can't set {key} to {value:?}")
|
||||
})
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int,
|
||||
Err(_) => {
|
||||
warn!(ctx, "dc_set_config(): invalid key");
|
||||
@@ -252,7 +257,8 @@ pub unsafe extern "C" fn dc_get_config(
|
||||
if key.starts_with("ui.") {
|
||||
ctx.get_ui_config(&key)
|
||||
.await
|
||||
.log_err(ctx, "Can't get ui-config")
|
||||
.context("Can't get ui-config")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
@@ -261,7 +267,8 @@ pub unsafe extern "C" fn dc_get_config(
|
||||
Ok(key) => ctx
|
||||
.get_config(key)
|
||||
.await
|
||||
.log_err(ctx, "Can't get config")
|
||||
.context("Can't get config")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default()
|
||||
.strdup(),
|
||||
@@ -413,7 +420,8 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
|
||||
block_on(async move {
|
||||
match oauth2::get_oauth2_url(ctx, &addr, &redirect)
|
||||
.await
|
||||
.log_err(ctx, "dc_get_oauth2_url failed")
|
||||
.context("dc_get_oauth2_url failed")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(Some(res)) => res.strdup(),
|
||||
Ok(None) | Err(_) => ptr::null_mut(),
|
||||
@@ -422,7 +430,12 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
|
||||
}
|
||||
|
||||
fn spawn_configure(ctx: Context) {
|
||||
spawn(async move { ctx.configure().await.log_err(&ctx, "Configure failed") });
|
||||
spawn(async move {
|
||||
ctx.configure()
|
||||
.await
|
||||
.context("Configure failed")
|
||||
.log_err(&ctx)
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -447,7 +460,8 @@ pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c
|
||||
block_on(async move {
|
||||
ctx.is_configured()
|
||||
.await
|
||||
.log_err(ctx, "failed to get configured state")
|
||||
.context("failed to get configured state")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default() as libc::c_int
|
||||
})
|
||||
}
|
||||
@@ -499,6 +513,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::SmtpMessageSent(_) => 103,
|
||||
EventType::ImapMessageDeleted(_) => 104,
|
||||
EventType::ImapMessageMoved(_) => 105,
|
||||
EventType::ImapInboxIdle => 106,
|
||||
EventType::NewBlobFile(_) => 150,
|
||||
EventType::DeletedBlobFile(_) => 151,
|
||||
EventType::Warning(_) => 300,
|
||||
@@ -543,6 +558,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::SmtpMessageSent(_)
|
||||
| EventType::ImapMessageDeleted(_)
|
||||
| EventType::ImapMessageMoved(_)
|
||||
| EventType::ImapInboxIdle
|
||||
| EventType::NewBlobFile(_)
|
||||
| EventType::DeletedBlobFile(_)
|
||||
| EventType::Warning(_)
|
||||
@@ -593,6 +609,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::SmtpMessageSent(_)
|
||||
| EventType::ImapMessageDeleted(_)
|
||||
| EventType::ImapMessageMoved(_)
|
||||
| EventType::ImapInboxIdle
|
||||
| EventType::NewBlobFile(_)
|
||||
| EventType::DeletedBlobFile(_)
|
||||
| EventType::Warning(_)
|
||||
@@ -652,6 +669,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
EventType::MsgsChanged { .. }
|
||||
| EventType::ReactionsChanged { .. }
|
||||
| EventType::IncomingMsg { .. }
|
||||
| EventType::ImapInboxIdle
|
||||
| EventType::MsgsNoticed(_)
|
||||
| EventType::MsgDelivered { .. }
|
||||
| EventType::MsgFailed { .. }
|
||||
@@ -785,7 +803,8 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default).await?;
|
||||
Ok::<_, anyhow::Error>(1)
|
||||
})
|
||||
.log_err(ctx, "Failed to save keypair")
|
||||
.context("Failed to save keypair")
|
||||
.log_err(ctx)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
@@ -812,7 +831,8 @@ pub unsafe extern "C" fn dc_get_chatlist(
|
||||
block_on(async move {
|
||||
match chatlist::Chatlist::try_load(ctx, flags as usize, qs.as_deref(), qi)
|
||||
.await
|
||||
.log_err(ctx, "Failed to get chatlist")
|
||||
.context("Failed to get chatlist")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(list) => {
|
||||
let ffi_list = ChatlistWrapper { context, list };
|
||||
@@ -837,7 +857,8 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id(
|
||||
block_on(async move {
|
||||
ChatId::create_for_contact(ctx, ContactId::new(contact_id))
|
||||
.await
|
||||
.log_err(ctx, "Failed to create chat from contact_id")
|
||||
.context("Failed to create chat from contact_id")
|
||||
.log_err(ctx)
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
@@ -857,7 +878,8 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
|
||||
block_on(async move {
|
||||
ChatId::lookup_by_contact(ctx, ContactId::new(contact_id))
|
||||
.await
|
||||
.log_err(ctx, "Failed to get chat for contact_id")
|
||||
.context("Failed to get chat for contact_id")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default() // unwraps the Result
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0) // unwraps the Option
|
||||
@@ -999,7 +1021,8 @@ pub unsafe extern "C" fn dc_get_msg_reactions(
|
||||
let ctx = &*context;
|
||||
|
||||
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
|
||||
.log_err(ctx, "failed dc_get_msg_reactions() call")
|
||||
.context("failed dc_get_msg_reactions() call")
|
||||
.log_err(ctx)
|
||||
{
|
||||
reactions
|
||||
} else {
|
||||
@@ -1027,7 +1050,8 @@ pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||
&to_string_lossy(json),
|
||||
&to_string_lossy(descr),
|
||||
))
|
||||
.log_err(ctx, "Failed to send webxdc update")
|
||||
.context("Failed to send webxdc update")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
@@ -1246,7 +1270,8 @@ pub unsafe extern "C" fn dc_get_fresh_msgs(
|
||||
let arr = dc_array_t::from(
|
||||
ctx.get_fresh_msgs()
|
||||
.await
|
||||
.log_err(ctx, "Failed to get fresh messages")
|
||||
.context("Failed to get fresh messages")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
@@ -1256,6 +1281,50 @@ pub unsafe extern "C" fn dc_get_fresh_msgs(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_next_msgs(context: *mut dc_context_t) -> *mut dc_array::dc_array_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_next_msgs()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
let msg_ids = block_on(ctx.get_next_msgs())
|
||||
.context("failed to get next messages")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default();
|
||||
let arr = dc_array_t::from(
|
||||
msg_ids
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
Box::into_raw(Box::new(arr))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_wait_next_msgs(
|
||||
context: *mut dc_context_t,
|
||||
) -> *mut dc_array::dc_array_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_wait_next_msgs()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
let msg_ids = block_on(ctx.wait_next_msgs())
|
||||
.context("failed to wait for next messages")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default();
|
||||
let arr = dc_array_t::from(
|
||||
msg_ids
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
Box::into_raw(Box::new(arr))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id: u32) {
|
||||
if context.is_null() {
|
||||
@@ -1267,7 +1336,8 @@ pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id
|
||||
block_on(async move {
|
||||
chat::marknoticed_chat(ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.log_err(ctx, "Failed marknoticed chat")
|
||||
.context("Failed marknoticed chat")
|
||||
.log_err(ctx)
|
||||
.unwrap_or(())
|
||||
})
|
||||
}
|
||||
@@ -1409,7 +1479,8 @@ pub unsafe extern "C" fn dc_set_chat_visibility(
|
||||
ChatId::new(chat_id)
|
||||
.set_visibility(ctx, visibility)
|
||||
.await
|
||||
.log_err(ctx, "Failed setting chat visibility")
|
||||
.context("Failed setting chat visibility")
|
||||
.log_err(ctx)
|
||||
.unwrap_or(())
|
||||
})
|
||||
}
|
||||
@@ -1426,7 +1497,9 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
|
||||
ChatId::new(chat_id)
|
||||
.delete(ctx)
|
||||
.await
|
||||
.ok_or_log_msg(ctx, "Failed chat delete");
|
||||
.context("Failed chat delete")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1442,7 +1515,9 @@ pub unsafe extern "C" fn dc_block_chat(context: *mut dc_context_t, chat_id: u32)
|
||||
ChatId::new(chat_id)
|
||||
.block(ctx)
|
||||
.await
|
||||
.ok_or_log_msg(ctx, "Failed chat block");
|
||||
.context("Failed chat block")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1458,7 +1533,9 @@ pub unsafe extern "C" fn dc_accept_chat(context: *mut dc_context_t, chat_id: u32
|
||||
ChatId::new(chat_id)
|
||||
.accept(ctx)
|
||||
.await
|
||||
.ok_or_log_msg(ctx, "Failed chat accept");
|
||||
.context("Failed chat accept")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1556,7 +1633,8 @@ pub unsafe extern "C" fn dc_create_group_chat(
|
||||
block_on(async move {
|
||||
chat::create_group_chat(ctx, protect, &to_string_lossy(name))
|
||||
.await
|
||||
.log_err(ctx, "Failed to create group chat")
|
||||
.context("Failed to create group chat")
|
||||
.log_err(ctx)
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
@@ -1570,7 +1648,8 @@ pub unsafe extern "C" fn dc_create_broadcast_list(context: *mut dc_context_t) ->
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(chat::create_broadcast_list(ctx))
|
||||
.log_err(ctx, "Failed to create broadcast list")
|
||||
.context("Failed to create broadcast list")
|
||||
.log_err(ctx)
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
@@ -1592,7 +1671,8 @@ pub unsafe extern "C" fn dc_is_contact_in_chat(
|
||||
ChatId::new(chat_id),
|
||||
ContactId::new(contact_id),
|
||||
))
|
||||
.log_err(ctx, "is_contact_in_chat failed")
|
||||
.context("is_contact_in_chat failed")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default() as libc::c_int
|
||||
}
|
||||
|
||||
@@ -1613,7 +1693,8 @@ pub unsafe extern "C" fn dc_add_contact_to_chat(
|
||||
ChatId::new(chat_id),
|
||||
ContactId::new(contact_id),
|
||||
))
|
||||
.log_err(ctx, "Failed to add contact")
|
||||
.context("Failed to add contact")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
@@ -1634,7 +1715,8 @@ pub unsafe extern "C" fn dc_remove_contact_from_chat(
|
||||
ChatId::new(chat_id),
|
||||
ContactId::new(contact_id),
|
||||
))
|
||||
.log_err(ctx, "Failed to remove contact")
|
||||
.context("Failed to remove contact")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
@@ -1753,7 +1835,8 @@ pub unsafe extern "C" fn dc_get_chat_ephemeral_timer(
|
||||
// ignored when ephemeral timer value is used to construct
|
||||
// message headers.
|
||||
block_on(async move { ChatId::new(chat_id).get_ephemeral_timer(ctx).await })
|
||||
.log_err(ctx, "Failed to get ephemeral timer")
|
||||
.context("Failed to get ephemeral timer")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
.to_u32()
|
||||
}
|
||||
@@ -1774,7 +1857,8 @@ pub unsafe extern "C" fn dc_set_chat_ephemeral_timer(
|
||||
ChatId::new(chat_id)
|
||||
.set_ephemeral_timer(ctx, EphemeralTimer::from_u32(timer))
|
||||
.await
|
||||
.log_err(ctx, "Failed to set ephemeral timer")
|
||||
.context("Failed to set ephemeral timer")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
})
|
||||
}
|
||||
@@ -1850,7 +1934,8 @@ pub unsafe extern "C" fn dc_delete_msgs(
|
||||
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
|
||||
|
||||
block_on(message::delete_msgs(ctx, &msg_ids))
|
||||
.log_err(ctx, "failed dc_delete_msgs() call")
|
||||
.context("failed dc_delete_msgs() call")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -1914,7 +1999,8 @@ pub unsafe extern "C" fn dc_markseen_msgs(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(message::markseen_msgs(ctx, msg_ids))
|
||||
.log_err(ctx, "failed dc_markseen_msgs() call")
|
||||
.context("failed dc_markseen_msgs() call")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -1956,7 +2042,8 @@ pub unsafe extern "C" fn dc_download_full_msg(context: *mut dc_context_t, msg_id
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(MsgId::new(msg_id).download_full(ctx))
|
||||
.log_err(ctx, "Failed to download message fully.")
|
||||
.context("Failed to download message fully.")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -2004,7 +2091,8 @@ pub unsafe extern "C" fn dc_create_contact(
|
||||
let name = to_string_lossy(name);
|
||||
|
||||
block_on(Contact::create(ctx, &name, &to_string_lossy(addr)))
|
||||
.log_err(ctx, "Cannot create contact")
|
||||
.context("Cannot create contact")
|
||||
.log_err(ctx)
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
@@ -2081,7 +2169,8 @@ pub unsafe extern "C" fn dc_get_blocked_contacts(
|
||||
Box::into_raw(Box::new(dc_array_t::from(
|
||||
Contact::get_all_blocked(ctx)
|
||||
.await
|
||||
.log_err(ctx, "Can't get blocked contacts")
|
||||
.context("Can't get blocked contacts")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|id| id.to_u32())
|
||||
@@ -2106,11 +2195,15 @@ pub unsafe extern "C" fn dc_block_contact(
|
||||
if block == 0 {
|
||||
Contact::unblock(ctx, contact_id)
|
||||
.await
|
||||
.ok_or_log_msg(ctx, "Can't unblock contact");
|
||||
.context("Can't unblock contact")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
} else {
|
||||
Contact::block(ctx, contact_id)
|
||||
.await
|
||||
.ok_or_log_msg(ctx, "Can't block contact");
|
||||
.context("Can't block contact")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2183,7 +2276,8 @@ fn spawn_imex(ctx: Context, what: imex::ImexMode, param1: String, passphrase: Op
|
||||
spawn(async move {
|
||||
imex::imex(&ctx, what, param1.as_ref(), passphrase)
|
||||
.await
|
||||
.log_err(&ctx, "IMEX failed")
|
||||
.context("IMEX failed")
|
||||
.log_err(&ctx)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2369,7 +2463,8 @@ pub unsafe extern "C" fn dc_join_securejoin(
|
||||
securejoin::join_securejoin(ctx, &to_string_lossy(qr))
|
||||
.await
|
||||
.map(|chatid| chatid.to_u32())
|
||||
.log_err(ctx, "failed dc_join_securejoin() call")
|
||||
.context("failed dc_join_securejoin() call")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
@@ -2391,7 +2486,8 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
|
||||
ChatId::new(chat_id),
|
||||
seconds as i64,
|
||||
))
|
||||
.log_err(ctx, "Failed dc_send_locations_to_chat()")
|
||||
.context("Failed dc_send_locations_to_chat()")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -2474,7 +2570,8 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
|
||||
block_on(async move {
|
||||
location::delete_all(ctx)
|
||||
.await
|
||||
.log_err(ctx, "Failed to delete locations")
|
||||
.context("Failed to delete locations")
|
||||
.log_err(ctx)
|
||||
.ok()
|
||||
});
|
||||
}
|
||||
@@ -2758,7 +2855,8 @@ pub unsafe extern "C" fn dc_chatlist_get_summary(
|
||||
.list
|
||||
.get_summary(ctx, index, maybe_chat)
|
||||
.await
|
||||
.log_err(ctx, "get_summary failed")
|
||||
.context("get_summary failed")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default();
|
||||
Box::into_raw(Box::new(summary.into()))
|
||||
})
|
||||
@@ -2786,7 +2884,8 @@ pub unsafe extern "C" fn dc_chatlist_get_summary2(
|
||||
msg_id,
|
||||
None,
|
||||
))
|
||||
.log_err(ctx, "get_summary2 failed")
|
||||
.context("get_summary2 failed")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default();
|
||||
Box::into_raw(Box::new(summary.into()))
|
||||
}
|
||||
@@ -2969,7 +3068,8 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
let ffi_chat = &*chat;
|
||||
let ctx = &*ffi_chat.context;
|
||||
block_on(ffi_chat.chat.can_send(ctx))
|
||||
.log_err(ctx, "can_send failed")
|
||||
.context("can_send failed")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default() as libc::c_int
|
||||
}
|
||||
|
||||
@@ -3414,7 +3514,8 @@ pub unsafe extern "C" fn dc_msg_get_summary(
|
||||
let ctx = &*ffi_msg.context;
|
||||
|
||||
let summary = block_on(ffi_msg.message.get_summary(ctx, maybe_chat))
|
||||
.log_err(ctx, "dc_msg_get_summary failed")
|
||||
.context("dc_msg_get_summary failed")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default();
|
||||
Box::into_raw(Box::new(summary.into()))
|
||||
}
|
||||
@@ -3432,7 +3533,8 @@ pub unsafe extern "C" fn dc_msg_get_summarytext(
|
||||
let ctx = &*ffi_msg.context;
|
||||
|
||||
let summary = block_on(ffi_msg.message.get_summary(ctx, None))
|
||||
.log_err(ctx, "dc_msg_get_summarytext failed")
|
||||
.context("dc_msg_get_summarytext failed")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default();
|
||||
match usize::try_from(approx_characters) {
|
||||
Ok(chars) => summary.truncated_text(chars).strdup(),
|
||||
@@ -3699,7 +3801,9 @@ pub unsafe extern "C" fn dc_msg_latefiling_mediasize(
|
||||
.message
|
||||
.latefiling_mediasize(ctx, width, height, duration)
|
||||
})
|
||||
.ok_or_log_msg(ctx, "Cannot set media size");
|
||||
.context("Cannot set media size")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3738,7 +3842,8 @@ pub unsafe extern "C" fn dc_msg_set_quote(msg: *mut dc_msg_t, quote: *const dc_m
|
||||
.message
|
||||
.set_quote(&*ffi_msg.context, quote_msg)
|
||||
.await
|
||||
.log_err(&*ffi_msg.context, "failed to set quote")
|
||||
.context("failed to set quote")
|
||||
.log_err(&*ffi_msg.context)
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
@@ -3769,7 +3874,8 @@ pub unsafe extern "C" fn dc_msg_get_quoted_msg(msg: *const dc_msg_t) -> *mut dc_
|
||||
.message
|
||||
.quoted_message(context)
|
||||
.await
|
||||
.log_err(context, "failed to get quoted message")
|
||||
.context("failed to get quoted message")
|
||||
.log_err(context)
|
||||
.unwrap_or(None)
|
||||
});
|
||||
|
||||
@@ -3792,7 +3898,8 @@ pub unsafe extern "C" fn dc_msg_get_parent(msg: *const dc_msg_t) -> *mut dc_msg_
|
||||
.message
|
||||
.parent(context)
|
||||
.await
|
||||
.log_err(context, "failed to get parent message")
|
||||
.context("failed to get parent message")
|
||||
.log_err(context)
|
||||
.unwrap_or(None)
|
||||
});
|
||||
|
||||
@@ -3983,7 +4090,8 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
|
||||
let ctx = &*ffi_contact.context;
|
||||
|
||||
block_on(ffi_contact.contact.is_verified(ctx))
|
||||
.log_err(ctx, "is_verified failed")
|
||||
.context("is_verified failed")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default() as libc::c_int
|
||||
}
|
||||
|
||||
@@ -3998,7 +4106,8 @@ pub unsafe extern "C" fn dc_contact_get_verifier_addr(
|
||||
let ffi_contact = &*contact;
|
||||
let ctx = &*ffi_contact.context;
|
||||
block_on(ffi_contact.contact.get_verifier_addr(ctx))
|
||||
.log_err(ctx, "failed to get verifier for contact")
|
||||
.context("failed to get verifier for contact")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
}
|
||||
@@ -4012,7 +4121,8 @@ pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t)
|
||||
let ffi_contact = &*contact;
|
||||
let ctx = &*ffi_contact.context;
|
||||
let verifier_contact_id = block_on(ffi_contact.contact.get_verifier_id(ctx))
|
||||
.log_err(ctx, "failed to get verifier")
|
||||
.context("failed to get verifier")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -4142,6 +4252,131 @@ pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
|
||||
libc::free(s as *mut _)
|
||||
}
|
||||
|
||||
pub struct BackupProviderWrapper {
|
||||
context: *const dc_context_t,
|
||||
provider: BackupProvider,
|
||||
}
|
||||
|
||||
pub type dc_backup_provider_t = BackupProviderWrapper;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_backup_provider_new(
|
||||
context: *mut dc_context_t,
|
||||
) -> *mut dc_backup_provider_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_backup_provider_new()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(BackupProvider::prepare(ctx))
|
||||
.map(|provider| BackupProviderWrapper {
|
||||
context: ctx,
|
||||
provider,
|
||||
})
|
||||
.map(|ffi_provider| Box::into_raw(Box::new(ffi_provider)))
|
||||
.context("BackupProvider failed")
|
||||
.log_err(ctx)
|
||||
.set_last_error(ctx)
|
||||
.unwrap_or(ptr::null_mut())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_backup_provider_get_qr(
|
||||
provider: *const dc_backup_provider_t,
|
||||
) -> *mut libc::c_char {
|
||||
if provider.is_null() {
|
||||
eprintln!("ignoring careless call to dc_backup_provider_qr");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_provider = &*provider;
|
||||
let ctx = &*ffi_provider.context;
|
||||
deltachat::qr::format_backup(&ffi_provider.provider.qr())
|
||||
.context("BackupProvider get_qr failed")
|
||||
.log_err(ctx)
|
||||
.set_last_error(ctx)
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_backup_provider_get_qr_svg(
|
||||
provider: *const dc_backup_provider_t,
|
||||
) -> *mut libc::c_char {
|
||||
if provider.is_null() {
|
||||
eprintln!("ignoring careless call to dc_backup_provider_qr_svg()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_provider = &*provider;
|
||||
let ctx = &*ffi_provider.context;
|
||||
let provider = &ffi_provider.provider;
|
||||
block_on(generate_backup_qr(ctx, &provider.qr()))
|
||||
.context("BackupProvider get_qr_svg failed")
|
||||
.log_err(ctx)
|
||||
.set_last_error(ctx)
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_backup_provider_wait(provider: *mut dc_backup_provider_t) {
|
||||
if provider.is_null() {
|
||||
eprintln!("ignoring careless call to dc_backup_provider_wait()");
|
||||
return;
|
||||
}
|
||||
let ffi_provider = &mut *provider;
|
||||
let ctx = &*ffi_provider.context;
|
||||
let provider = &mut ffi_provider.provider;
|
||||
block_on(provider)
|
||||
.context("Failed to await BackupProvider")
|
||||
.log_err(ctx)
|
||||
.set_last_error(ctx)
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_backup_provider_unref(provider: *mut dc_backup_provider_t) {
|
||||
if provider.is_null() {
|
||||
eprintln!("ignoring careless call to dc_backup_provider_unref()");
|
||||
return;
|
||||
}
|
||||
drop(Box::from_raw(provider));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_receive_backup(
|
||||
context: *mut dc_context_t,
|
||||
qr: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_receive_backup()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let qr_text = to_string_lossy(qr);
|
||||
receive_backup(ctx.clone(), qr_text)
|
||||
}
|
||||
|
||||
// Because this is a long-running operation make sure we own the Context. This stops a FFI
|
||||
// user from deallocating it by calling unref on the object while we are using it.
|
||||
fn receive_backup(ctx: Context, qr_text: String) -> libc::c_int {
|
||||
let qr = match block_on(qr::check_qr(&ctx, &qr_text))
|
||||
.context("Invalid QR code")
|
||||
.log_err(&ctx)
|
||||
.set_last_error(&ctx)
|
||||
{
|
||||
Ok(qr) => qr,
|
||||
Err(_) => return 0,
|
||||
};
|
||||
match block_on(imex::get_backup(&ctx, qr))
|
||||
.context("Get backup failed")
|
||||
.log_err(&ctx)
|
||||
.set_last_error(&ctx)
|
||||
{
|
||||
Ok(_) => 1,
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
trait ResultExt<T, E> {
|
||||
/// Like `log_err()`, but:
|
||||
/// - returns the default value instead of an Err value.
|
||||
@@ -4162,6 +4397,56 @@ impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
|
||||
}
|
||||
}
|
||||
|
||||
trait ResultLastError<T, E>
|
||||
where
|
||||
E: std::fmt::Display,
|
||||
{
|
||||
/// Sets this `Err` value using [`Context::set_last_error`].
|
||||
///
|
||||
/// Normally each FFI-API *should* call this if it handles an error from the Rust API:
|
||||
/// errors which need to be reported to users in response to an API call need to be
|
||||
/// propagated up the Rust API and at the FFI boundary need to be stored into the "last
|
||||
/// error" so the FFI users can retrieve an appropriate error message on failure. Often
|
||||
/// you will want to combine this with a call to [`LogExt::log_err`].
|
||||
///
|
||||
/// Since historically calls to the `deltachat::log::error!()` macro were (and sometimes
|
||||
/// still are) shown as error toasts to the user, this macro also calls
|
||||
/// [`Context::set_last_error`]. It is preferable however to rely on normal error
|
||||
/// propagation in Rust code however and only use this `ResultExt::set_last_error` call
|
||||
/// in the FFI layer.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Fully handling an error in the FFI code looks like this currently:
|
||||
///
|
||||
/// ```no_compile
|
||||
/// some_dc_rust_api_call_returning_result()
|
||||
/// .context("My API call failed")
|
||||
/// .log_err(&context)
|
||||
/// .set_last_error(&context)
|
||||
/// .unwrap_or_default()
|
||||
/// ```
|
||||
///
|
||||
/// As shows it is a shame the `.log_err()` call currently needs a message instead of
|
||||
/// relying on an implicit call to the [`anyhow::Context`] call if needed. This stems
|
||||
/// from a time before we fully embraced anyhow. Some day we'll also fix that.
|
||||
///
|
||||
/// [`Context::set_last_error`]: context::Context::set_last_error
|
||||
fn set_last_error(self, context: &context::Context) -> Result<T, E>;
|
||||
}
|
||||
|
||||
impl<T, E> ResultLastError<T, E> for Result<T, E>
|
||||
where
|
||||
E: std::fmt::Display,
|
||||
{
|
||||
fn set_last_error(self, context: &context::Context) -> Result<T, E> {
|
||||
if let Err(ref err) = self {
|
||||
context.set_last_error(&format!("{err:#}"));
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
trait ResultNullableExt<T> {
|
||||
fn into_raw(self) -> *mut T;
|
||||
}
|
||||
@@ -4224,7 +4509,8 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
|
||||
let socks5_enabled = block_on(async move {
|
||||
ctx.get_config_bool(config::Config::Socks5Enabled)
|
||||
.await
|
||||
.log_err(ctx, "Can't get config")
|
||||
.context("Can't get config")
|
||||
.log_err(ctx)
|
||||
});
|
||||
|
||||
match socks5_enabled {
|
||||
@@ -4287,6 +4573,96 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
|
||||
// this may change once we start localizing string.
|
||||
}
|
||||
|
||||
// dc_http_response_t
|
||||
|
||||
pub type dc_http_response_t = net::HttpResponse;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_http_response(
|
||||
context: *const dc_context_t,
|
||||
url: *const libc::c_char,
|
||||
) -> *mut dc_http_response_t {
|
||||
if context.is_null() || url.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_http_response()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let context = &*context;
|
||||
let url = to_string_lossy(url);
|
||||
if let Ok(response) = block_on(read_url_blob(context, &url))
|
||||
.context("read_url_blob")
|
||||
.log_err(context)
|
||||
{
|
||||
Box::into_raw(Box::new(response))
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_http_response_get_mimetype(
|
||||
response: *const dc_http_response_t,
|
||||
) -> *mut libc::c_char {
|
||||
if response.is_null() {
|
||||
eprintln!("ignoring careless call to dc_http_response_get_mimetype()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let response = &*response;
|
||||
response.mimetype.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_http_response_get_encoding(
|
||||
response: *const dc_http_response_t,
|
||||
) -> *mut libc::c_char {
|
||||
if response.is_null() {
|
||||
eprintln!("ignoring careless call to dc_http_response_get_encoding()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let response = &*response;
|
||||
response.encoding.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_http_response_get_blob(
|
||||
response: *const dc_http_response_t,
|
||||
) -> *mut libc::c_char {
|
||||
if response.is_null() {
|
||||
eprintln!("ignoring careless call to dc_http_response_get_blob()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let response = &*response;
|
||||
let blob_len = response.blob.len();
|
||||
let ptr = libc::malloc(blob_len);
|
||||
libc::memcpy(ptr, response.blob.as_ptr() as *mut libc::c_void, blob_len);
|
||||
ptr as *mut libc::c_char
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_http_response_get_size(
|
||||
response: *const dc_http_response_t,
|
||||
) -> libc::size_t {
|
||||
if response.is_null() {
|
||||
eprintln!("ignoring careless call to dc_http_response_get_size()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let response = &*response;
|
||||
response.blob.len()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_http_response_unref(response: *mut dc_http_response_t) {
|
||||
if response.is_null() {
|
||||
eprintln!("ignoring careless call to dc_http_response_unref()");
|
||||
return;
|
||||
}
|
||||
drop(Box::from_raw(response));
|
||||
}
|
||||
|
||||
// -- Accounts
|
||||
|
||||
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
|
||||
@@ -4591,7 +4967,6 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
#[cfg(feature = "jsonrpc")]
|
||||
mod jsonrpc {
|
||||
use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::events::event_to_json_rpc_notification;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
|
||||
use super::*;
|
||||
@@ -4599,7 +4974,6 @@ mod jsonrpc {
|
||||
pub struct dc_jsonrpc_instance_t {
|
||||
receiver: OutReceiver,
|
||||
handle: RpcSession<CommandApi>,
|
||||
event_thread: JoinHandle<Result<(), anyhow::Error>>,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -4612,28 +4986,12 @@ mod jsonrpc {
|
||||
}
|
||||
|
||||
let account_manager = &*account_manager;
|
||||
let events = block_on(account_manager.read()).get_event_emitter();
|
||||
let cmd_api = deltachat_jsonrpc::api::CommandApi::from_arc(account_manager.inner.clone());
|
||||
|
||||
let (request_handle, receiver) = RpcClient::new();
|
||||
let handle = RpcSession::new(request_handle.clone(), cmd_api);
|
||||
let handle = RpcSession::new(request_handle, cmd_api);
|
||||
|
||||
let event_thread = spawn(async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
let event = event_to_json_rpc_notification(event);
|
||||
request_handle
|
||||
.send_notification("event", Some(event))
|
||||
.await?;
|
||||
}
|
||||
let res: Result<(), anyhow::Error> = Ok(());
|
||||
res
|
||||
});
|
||||
|
||||
let instance = dc_jsonrpc_instance_t {
|
||||
receiver,
|
||||
handle,
|
||||
event_thread,
|
||||
};
|
||||
let instance = dc_jsonrpc_instance_t { receiver, handle };
|
||||
|
||||
Box::into_raw(Box::new(instance))
|
||||
}
|
||||
@@ -4644,7 +5002,6 @@ mod jsonrpc {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
|
||||
return;
|
||||
}
|
||||
(*jsonrpc_instance).event_thread.abort();
|
||||
drop(Box::from_raw(jsonrpc_instance));
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ use crate::summary::{Summary, SummaryPrefix};
|
||||
/// eg. by chatlist.get_summary() or dc_msg_get_summary().
|
||||
///
|
||||
/// *Lot* is used in the meaning *heap* here.
|
||||
// The QR code grew too large. So be it.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum Lot {
|
||||
Summary(Summary),
|
||||
@@ -47,6 +49,7 @@ impl Lot {
|
||||
Qr::FprMismatch { .. } => None,
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||
Qr::Account { domain } => Some(domain),
|
||||
Qr::Backup { .. } => None,
|
||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||
Qr::Addr { draft, .. } => draft.as_deref(),
|
||||
Qr::Url { url } => Some(url),
|
||||
@@ -98,6 +101,7 @@ impl Lot {
|
||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::Backup { .. } => LotState::QrBackup,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
Qr::Url { .. } => LotState::QrUrl,
|
||||
@@ -122,6 +126,7 @@ impl Lot {
|
||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::Backup { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::Url { .. } => Default::default(),
|
||||
@@ -170,6 +175,8 @@ pub enum LotState {
|
||||
/// text1=domain
|
||||
QrAccount = 250,
|
||||
|
||||
QrBackup = 251,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.111.0"
|
||||
version = "1.114.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
@@ -19,21 +19,21 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tempfile = "3.3.0"
|
||||
log = "0.4"
|
||||
async-channel = { version = "1.8.0" }
|
||||
futures = { version = "0.3.26" }
|
||||
serde_json = "1.0.91"
|
||||
futures = { version = "0.3.28" }
|
||||
serde_json = "1.0.96"
|
||||
yerpc = { version = "0.4.3", features = ["anyhow_expose"] }
|
||||
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
|
||||
tokio = { version = "1.25.0" }
|
||||
tokio = { version = "1.28.0" }
|
||||
sanitize-filename = "0.4"
|
||||
walkdir = "2.3.2"
|
||||
walkdir = "2.3.3"
|
||||
base64 = "0.21"
|
||||
|
||||
# optional dependencies
|
||||
axum = { version = "0.6.6", optional = true, features = ["ws"] }
|
||||
axum = { version = "0.6.18", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.10.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.25.0", features = ["full", "rt-multi-thread"] }
|
||||
tokio = { version = "1.28.0", features = ["full", "rt-multi-thread"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
use deltachat::{Event, EventType};
|
||||
use deltachat::{Event as CoreEvent, EventType as CoreEventType};
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
pub fn event_to_json_rpc_notification(event: Event) -> Value {
|
||||
let id: JSONRPCEventType = event.typ.into();
|
||||
json!({
|
||||
"event": id,
|
||||
"contextId": event.id,
|
||||
})
|
||||
#[derive(Serialize, TypeDef)]
|
||||
pub struct Event {
|
||||
/// Event payload.
|
||||
event: EventType,
|
||||
|
||||
/// Account ID.
|
||||
context_id: u32,
|
||||
}
|
||||
|
||||
impl From<CoreEvent> for Event {
|
||||
fn from(event: CoreEvent) -> Self {
|
||||
Event {
|
||||
event: event.typ.into(),
|
||||
context_id: event.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(tag = "type", rename = "Event")]
|
||||
pub enum JSONRPCEventType {
|
||||
#[serde(tag = "type")]
|
||||
pub enum EventType {
|
||||
/// The library-user may write an informational string to the log.
|
||||
///
|
||||
/// This event should *not* be reported to the end-user using a popup or something like
|
||||
@@ -47,6 +56,9 @@ pub enum JSONRPCEventType {
|
||||
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,
|
||||
@@ -283,26 +295,27 @@ pub enum JSONRPCEventType {
|
||||
},
|
||||
}
|
||||
|
||||
impl From<EventType> for JSONRPCEventType {
|
||||
fn from(event: EventType) -> Self {
|
||||
use JSONRPCEventType::*;
|
||||
impl From<CoreEventType> for EventType {
|
||||
fn from(event: CoreEventType) -> Self {
|
||||
use EventType::*;
|
||||
match event {
|
||||
EventType::Info(msg) => Info { msg },
|
||||
EventType::SmtpConnected(msg) => SmtpConnected { msg },
|
||||
EventType::ImapConnected(msg) => ImapConnected { msg },
|
||||
EventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
|
||||
EventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
|
||||
EventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
|
||||
EventType::NewBlobFile(file) => NewBlobFile { file },
|
||||
EventType::DeletedBlobFile(file) => DeletedBlobFile { file },
|
||||
EventType::Warning(msg) => Warning { msg },
|
||||
EventType::Error(msg) => Error { msg },
|
||||
EventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
|
||||
EventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
|
||||
CoreEventType::Info(msg) => Info { msg },
|
||||
CoreEventType::SmtpConnected(msg) => SmtpConnected { msg },
|
||||
CoreEventType::ImapConnected(msg) => ImapConnected { msg },
|
||||
CoreEventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
|
||||
CoreEventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
|
||||
CoreEventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
|
||||
CoreEventType::ImapInboxIdle => ImapInboxIdle,
|
||||
CoreEventType::NewBlobFile(file) => NewBlobFile { file },
|
||||
CoreEventType::DeletedBlobFile(file) => DeletedBlobFile { file },
|
||||
CoreEventType::Warning(msg) => Warning { msg },
|
||||
CoreEventType::Error(msg) => Error { msg },
|
||||
CoreEventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
|
||||
CoreEventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
EventType::ReactionsChanged {
|
||||
CoreEventType::ReactionsChanged {
|
||||
chat_id,
|
||||
msg_id,
|
||||
contact_id,
|
||||
@@ -311,92 +324,76 @@ impl From<EventType> for JSONRPCEventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
contact_id: contact_id.to_u32(),
|
||||
},
|
||||
EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
||||
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
EventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
|
||||
CoreEventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
|
||||
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
|
||||
},
|
||||
EventType::MsgsNoticed(chat_id) => MsgsNoticed {
|
||||
CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed {
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
EventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
|
||||
CoreEventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
EventType::MsgFailed { chat_id, msg_id } => MsgFailed {
|
||||
CoreEventType::MsgFailed { chat_id, msg_id } => MsgFailed {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
EventType::MsgRead { chat_id, msg_id } => MsgRead {
|
||||
CoreEventType::MsgRead { chat_id, msg_id } => MsgRead {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
EventType::ChatModified(chat_id) => ChatModified {
|
||||
CoreEventType::ChatModified(chat_id) => ChatModified {
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
EventType::ChatEphemeralTimerModified { chat_id, timer } => {
|
||||
CoreEventType::ChatEphemeralTimerModified { chat_id, timer } => {
|
||||
ChatEphemeralTimerModified {
|
||||
chat_id: chat_id.to_u32(),
|
||||
timer: timer.to_u32(),
|
||||
}
|
||||
}
|
||||
EventType::ContactsChanged(contact) => ContactsChanged {
|
||||
CoreEventType::ContactsChanged(contact) => ContactsChanged {
|
||||
contact_id: contact.map(|c| c.to_u32()),
|
||||
},
|
||||
EventType::LocationChanged(contact) => LocationChanged {
|
||||
CoreEventType::LocationChanged(contact) => LocationChanged {
|
||||
contact_id: contact.map(|c| c.to_u32()),
|
||||
},
|
||||
EventType::ConfigureProgress { progress, comment } => {
|
||||
CoreEventType::ConfigureProgress { progress, comment } => {
|
||||
ConfigureProgress { progress, comment }
|
||||
}
|
||||
EventType::ImexProgress(progress) => ImexProgress { progress },
|
||||
EventType::ImexFileWritten(path) => ImexFileWritten {
|
||||
CoreEventType::ImexProgress(progress) => ImexProgress { progress },
|
||||
CoreEventType::ImexFileWritten(path) => ImexFileWritten {
|
||||
path: path.to_str().unwrap_or_default().to_owned(),
|
||||
},
|
||||
EventType::SecurejoinInviterProgress {
|
||||
CoreEventType::SecurejoinInviterProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => SecurejoinInviterProgress {
|
||||
contact_id: contact_id.to_u32(),
|
||||
progress,
|
||||
},
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
CoreEventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => SecurejoinJoinerProgress {
|
||||
contact_id: contact_id.to_u32(),
|
||||
progress,
|
||||
},
|
||||
EventType::ConnectivityChanged => ConnectivityChanged,
|
||||
EventType::SelfavatarChanged => SelfavatarChanged,
|
||||
EventType::WebxdcStatusUpdate {
|
||||
CoreEventType::ConnectivityChanged => ConnectivityChanged,
|
||||
CoreEventType::SelfavatarChanged => SelfavatarChanged,
|
||||
CoreEventType::WebxdcStatusUpdate {
|
||||
msg_id,
|
||||
status_update_serial,
|
||||
} => WebxdcStatusUpdate {
|
||||
msg_id: msg_id.to_u32(),
|
||||
status_update_serial: status_update_serial.to_u32(),
|
||||
},
|
||||
EventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
||||
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn generate_events_ts_types_definition() {
|
||||
let events = {
|
||||
let mut buf = Vec::new();
|
||||
let options = typescript_type_def::DefinitionFileOptions {
|
||||
root_namespace: None,
|
||||
..typescript_type_def::DefinitionFileOptions::default()
|
||||
};
|
||||
typescript_type_def::write_definition_file::<_, JSONRPCEventType>(&mut buf, options)
|
||||
.unwrap();
|
||||
String::from_utf8(buf).unwrap()
|
||||
};
|
||||
std::fs::write("typescript/generated/events.ts", events).unwrap();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
pub use deltachat::accounts::Accounts;
|
||||
use deltachat::qr::Qr;
|
||||
use deltachat::{
|
||||
chat::{
|
||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
||||
@@ -22,14 +23,15 @@ use deltachat::{
|
||||
},
|
||||
provider::get_provider_info,
|
||||
qr,
|
||||
qr_code_generator::get_securejoin_qr_svg,
|
||||
reaction::send_reaction,
|
||||
qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
|
||||
reaction::{get_msg_reactions, send_reaction},
|
||||
securejoin,
|
||||
stock_str::StockMessage,
|
||||
webxdc::StatusUpdateSerial,
|
||||
};
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::{fs, sync::RwLock};
|
||||
use tokio::fs;
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
use walkdir::WalkDir;
|
||||
use yerpc::rpc;
|
||||
|
||||
@@ -39,13 +41,15 @@ pub mod types;
|
||||
use num_traits::FromPrimitive;
|
||||
use types::account::Account;
|
||||
use types::chat::FullChat;
|
||||
use types::chat_list::ChatListEntry;
|
||||
use types::contact::ContactObject;
|
||||
use types::http::HttpResponse;
|
||||
use types::message::MessageData;
|
||||
use types::message::MessageObject;
|
||||
use types::provider_info::ProviderInfo;
|
||||
use types::reactions::JSONRPCReactions;
|
||||
use types::webxdc::WebxdcMessageInfo;
|
||||
|
||||
use self::events::Event;
|
||||
use self::types::message::MessageLoadResult;
|
||||
use self::types::{
|
||||
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
|
||||
@@ -57,21 +61,45 @@ use self::types::{
|
||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||
use crate::api::types::qr::QrObject;
|
||||
|
||||
#[derive(Debug)]
|
||||
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>,
|
||||
}
|
||||
|
||||
impl Default for AccountState {
|
||||
fn default() -> Self {
|
||||
let (tx, _rx) = watch::channel(ProviderQr::NoProvider);
|
||||
Self {
|
||||
backup_provider_qr: tx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CommandApi {
|
||||
pub(crate) accounts: Arc<RwLock<Accounts>>,
|
||||
|
||||
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
|
||||
}
|
||||
|
||||
impl CommandApi {
|
||||
pub fn new(accounts: Accounts) -> Self {
|
||||
CommandApi {
|
||||
accounts: Arc::new(RwLock::new(accounts)),
|
||||
states: Arc::new(Mutex::new(BTreeMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
|
||||
CommandApi { accounts }
|
||||
CommandApi {
|
||||
accounts,
|
||||
states: Arc::new(Mutex::new(BTreeMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> {
|
||||
@@ -83,6 +111,38 @@ impl CommandApi {
|
||||
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
|
||||
Ok(sc)
|
||||
}
|
||||
|
||||
async fn with_state<F, T>(&self, id: u32, with_state: F) -> T
|
||||
where
|
||||
F: FnOnce(&AccountState) -> T,
|
||||
{
|
||||
let mut states = self.states.lock().await;
|
||||
let state = states.entry(id).or_insert_with(Default::default);
|
||||
with_state(state)
|
||||
}
|
||||
|
||||
async fn inner_get_backup_qr(&self, account_id: u32) -> Result<Qr> {
|
||||
let mut receiver = self
|
||||
.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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rpc(all_positional, ts_outdir = "typescript/generated")]
|
||||
@@ -106,6 +166,16 @@ impl CommandApi {
|
||||
get_info()
|
||||
}
|
||||
|
||||
/// Get the next event.
|
||||
async fn get_next_event(&self) -> Result<Event> {
|
||||
let event_emitter = self.accounts.read().await.get_event_emitter();
|
||||
event_emitter
|
||||
.recv()
|
||||
.await
|
||||
.map(|event| event.into())
|
||||
.context("event channel is closed")
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Account Management
|
||||
// ---------------------------------------------
|
||||
@@ -115,7 +185,13 @@ impl CommandApi {
|
||||
}
|
||||
|
||||
async fn remove_account(&self, account_id: u32) -> Result<()> {
|
||||
self.accounts.write().await.remove_account(account_id).await
|
||||
self.accounts
|
||||
.write()
|
||||
.await
|
||||
.remove_account(account_id)
|
||||
.await?;
|
||||
self.states.lock().await.remove(&account_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_all_account_ids(&self) -> Vec<u32> {
|
||||
@@ -141,8 +217,6 @@ impl CommandApi {
|
||||
let context_option = self.accounts.read().await.get_account(id);
|
||||
if let Some(ctx) = context_option {
|
||||
accounts.push(Account::from_context(&ctx, id).await?)
|
||||
} else {
|
||||
println!("account with id {id} doesn't exist anymore");
|
||||
}
|
||||
}
|
||||
Ok(accounts)
|
||||
@@ -389,6 +463,49 @@ impl CommandApi {
|
||||
ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await
|
||||
}
|
||||
|
||||
/// Gets messages to be processed by the bot and returns their IDs.
|
||||
///
|
||||
/// Only messages with database ID higher than `last_msg_id` config value
|
||||
/// are returned. After processing the messages, the bot should
|
||||
/// update `last_msg_id` by calling [`markseen_msgs`]
|
||||
/// or manually updating the value to avoid getting already
|
||||
/// processed messages.
|
||||
///
|
||||
/// [`markseen_msgs`]: Self::markseen_msgs
|
||||
async fn get_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg_ids = ctx
|
||||
.get_next_msgs()
|
||||
.await?
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect();
|
||||
Ok(msg_ids)
|
||||
}
|
||||
|
||||
/// Waits for messages to be processed by the bot and returns their IDs.
|
||||
///
|
||||
/// This function is similar to [`get_next_msgs`],
|
||||
/// but waits for internal new message notification before returning.
|
||||
/// New message notification is sent when new message is added to the database,
|
||||
/// on initialization, when I/O is started and when I/O is stopped.
|
||||
/// This allows bots to use `wait_next_msgs` in a loop to process
|
||||
/// old messages after initialization and during the bot runtime.
|
||||
/// To shutdown the bot, stopping I/O can be used to interrupt
|
||||
/// pending or next `wait_next_msgs` call.
|
||||
///
|
||||
/// [`get_next_msgs`]: Self::get_next_msgs
|
||||
async fn wait_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg_ids = ctx
|
||||
.wait_next_msgs()
|
||||
.await?
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect();
|
||||
Ok(msg_ids)
|
||||
}
|
||||
|
||||
/// Estimate the number of messages that will be deleted
|
||||
/// by the set_config()-options `delete_device_after` or `delete_server_after`.
|
||||
/// This is typically used to show the estimated impact to the user
|
||||
@@ -432,7 +549,7 @@ impl CommandApi {
|
||||
list_flags: Option<u32>,
|
||||
query_string: Option<String>,
|
||||
query_contact_id: Option<u32>,
|
||||
) -> Result<Vec<ChatListEntry>> {
|
||||
) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let list = Chatlist::try_load(
|
||||
&ctx,
|
||||
@@ -441,12 +558,9 @@ impl CommandApi {
|
||||
query_contact_id.map(ContactId::new),
|
||||
)
|
||||
.await?;
|
||||
let mut l: Vec<ChatListEntry> = Vec::with_capacity(list.len());
|
||||
let mut l: Vec<u32> = Vec::with_capacity(list.len());
|
||||
for i in 0..list.len() {
|
||||
l.push(ChatListEntry(
|
||||
list.get_chat_id(i)?.to_u32(),
|
||||
list.get_msg_id(i)?.unwrap_or_default().to_u32(),
|
||||
));
|
||||
l.push(list.get_chat_id(i)?.to_u32());
|
||||
}
|
||||
Ok(l)
|
||||
}
|
||||
@@ -454,19 +568,18 @@ impl CommandApi {
|
||||
async fn get_chatlist_items_by_entries(
|
||||
&self,
|
||||
account_id: u32,
|
||||
entries: Vec<ChatListEntry>,
|
||||
entries: Vec<u32>,
|
||||
) -> Result<HashMap<u32, ChatListItemFetchResult>> {
|
||||
// todo custom json deserializer for ChatListEntry?
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut result: HashMap<u32, ChatListItemFetchResult> =
|
||||
HashMap::with_capacity(entries.len());
|
||||
for entry in entries.iter() {
|
||||
for &entry in entries.iter() {
|
||||
result.insert(
|
||||
entry.0,
|
||||
entry,
|
||||
match get_chat_list_item_by_id(&ctx, entry).await {
|
||||
Ok(res) => res,
|
||||
Err(err) => ChatListItemFetchResult::Error {
|
||||
id: entry.0,
|
||||
id: entry,
|
||||
error: format!("{err:#}"),
|
||||
},
|
||||
},
|
||||
@@ -880,6 +993,11 @@ impl CommandApi {
|
||||
/// Moreover, timer is started for incoming ephemeral messages.
|
||||
/// This also happens for contact requests chats.
|
||||
///
|
||||
/// This function updates `last_msg_id` configuration value
|
||||
/// to the maximum of the current value and IDs passed to this function.
|
||||
/// Bots which mark messages as seen can rely on this side effect
|
||||
/// to avoid updating `last_msg_id` value manually.
|
||||
///
|
||||
/// One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
|
||||
async fn markseen_msgs(&self, account_id: u32, msg_ids: Vec<u32>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -1018,17 +1136,17 @@ impl CommandApi {
|
||||
}
|
||||
|
||||
/// Search messages containing the given query string.
|
||||
/// Searching can be done globally (chat_id=0) or in a specified chat only (chat_id set).
|
||||
/// Searching can be done globally (chat_id=None) or in a specified chat only (chat_id set).
|
||||
///
|
||||
/// Global chat results are typically displayed using dc_msg_get_summary(), chat
|
||||
/// search results may just hilite the corresponding messages and present a
|
||||
/// Global search results are typically displayed using dc_msg_get_summary(), chat
|
||||
/// search results may just highlight the corresponding messages and present a
|
||||
/// prev/next button.
|
||||
///
|
||||
/// For global search, result is limited to 1000 messages,
|
||||
/// this allows incremental search done fast.
|
||||
/// So, when getting exactly 1000 results, the result may be truncated;
|
||||
/// the UIs may display sth. as "1000+ messages found" in this case.
|
||||
/// Chat search (if a chat_id is set) is not limited.
|
||||
/// For the global search, the result is limited to 1000 messages,
|
||||
/// this allows an incremental search done fast.
|
||||
/// So, when getting exactly 1000 messages, the result actually may be truncated;
|
||||
/// the UIs may display sth. like "1000+ messages found" in this case.
|
||||
/// The chat search (if chat_id is set) is not limited.
|
||||
async fn search_messages(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1325,16 +1443,13 @@ impl CommandApi {
|
||||
passphrase: Option<String>,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.stop_io().await;
|
||||
let result = imex::imex(
|
||||
imex::imex(
|
||||
&ctx,
|
||||
imex::ImexMode::ExportBackup,
|
||||
destination.as_ref(),
|
||||
passphrase,
|
||||
)
|
||||
.await;
|
||||
ctx.start_io().await;
|
||||
result
|
||||
.await
|
||||
}
|
||||
|
||||
async fn import_backup(
|
||||
@@ -1353,6 +1468,75 @@ impl CommandApi {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Offers a backup for remote devices to retrieve.
|
||||
///
|
||||
/// Can be cancelled by stopping the ongoing process. Success or failure can be tracked
|
||||
/// via the `ImexProgress` event which should either reach `1000` for success or `0` for
|
||||
/// failure.
|
||||
///
|
||||
/// This **stops IO** while it is running.
|
||||
///
|
||||
/// Returns once a remote device has retrieved the backup, or is 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()));
|
||||
})
|
||||
.await;
|
||||
|
||||
provider.await
|
||||
}
|
||||
|
||||
/// Returns the text of the QR code for the running [`CommandApi::provide_backup`].
|
||||
///
|
||||
/// 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.
|
||||
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
|
||||
let qr = self.inner_get_backup_qr(account_id).await?;
|
||||
qr::format_backup(&qr)
|
||||
}
|
||||
|
||||
/// Returns the rendered QR code for the running [`CommandApi::provide_backup`].
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// 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?;
|
||||
generate_backup_qr(&ctx, &qr).await
|
||||
}
|
||||
|
||||
/// Gets a backup from a remote provider.
|
||||
///
|
||||
/// This retrieves the backup from a remote device over the network and imports it into
|
||||
/// the current device.
|
||||
///
|
||||
/// Can be cancelled by stopping the ongoing process.
|
||||
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?;
|
||||
imex::get_backup(&ctx, qr).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// connectivity
|
||||
// ---------------------------------------------
|
||||
@@ -1480,6 +1664,15 @@ impl CommandApi {
|
||||
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
|
||||
}
|
||||
|
||||
/// Makes an HTTP GET request and returns a response.
|
||||
///
|
||||
/// `url` is the HTTP or HTTPS URL.
|
||||
async fn get_http_response(&self, account_id: u32, url: String) -> Result<HttpResponse> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let response = deltachat::net::read_url_blob(&ctx, &url).await?.into();
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Forward messages to another chat.
|
||||
///
|
||||
/// All types of messages can be forwarded,
|
||||
@@ -1529,6 +1722,21 @@ impl CommandApi {
|
||||
Ok(message_id.to_u32())
|
||||
}
|
||||
|
||||
/// Returns reactions to the message.
|
||||
async fn get_message_reactions(
|
||||
&self,
|
||||
account_id: u32,
|
||||
message_id: u32,
|
||||
) -> Result<Option<JSONRPCReactions>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let reactions = get_msg_reactions(&ctx, MsgId::new(message_id)).await?;
|
||||
if reactions.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(reactions.into()))
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -1571,6 +1779,15 @@ impl CommandApi {
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
/// Checks if messages can be sent to a given chat.
|
||||
async fn can_send(&self, account_id: u32, chat_id: u32) -> Result<bool> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
let chat = Chat::load_from_db(&ctx, chat_id).await?;
|
||||
let can_send = chat.can_send(&ctx).await?;
|
||||
Ok(can_send)
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// functions for the composer
|
||||
// the composer is the message input field
|
||||
@@ -1840,3 +2057,15 @@ 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),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
|
||||
use deltachat::chat::{Chat, ChatId};
|
||||
use deltachat::constants::Chattype;
|
||||
@@ -53,7 +53,9 @@ impl FullChat {
|
||||
contacts.push(
|
||||
ContactObject::try_from_dc_contact(
|
||||
context,
|
||||
Contact::load_from_db(context, *contact_id).await?,
|
||||
Contact::load_from_db(context, *contact_id)
|
||||
.await
|
||||
.context("failed to load contact")?,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -73,7 +75,8 @@ impl FullChat {
|
||||
let was_seen_recently = if chat.get_type() == Chattype::Single {
|
||||
match contact_ids.get(0) {
|
||||
Some(contact) => Contact::load_from_db(context, *contact)
|
||||
.await?
|
||||
.await
|
||||
.context("failed to load contact for was_seen_recently")?
|
||||
.was_seen_recently(),
|
||||
None => false,
|
||||
}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use deltachat::chat::{Chat, ChatId};
|
||||
use deltachat::chatlist::get_last_message_for_chat;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::{Contact, ContactId};
|
||||
use deltachat::{
|
||||
chat::{get_chat_contacts, ChatVisibility},
|
||||
chatlist::Chatlist,
|
||||
};
|
||||
use deltachat::{
|
||||
chat::{Chat, ChatId},
|
||||
message::MsgId,
|
||||
};
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
|
||||
#[derive(Deserialize, Serialize, TypeDef)]
|
||||
pub struct ChatListEntry(pub u32, pub u32);
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ChatListItemFetchResult {
|
||||
@@ -56,14 +51,9 @@ pub enum ChatListItemFetchResult {
|
||||
|
||||
pub(crate) async fn get_chat_list_item_by_id(
|
||||
ctx: &deltachat::context::Context,
|
||||
entry: &ChatListEntry,
|
||||
entry: u32,
|
||||
) -> Result<ChatListItemFetchResult> {
|
||||
let chat_id = ChatId::new(entry.0);
|
||||
let last_msgid = match entry.1 {
|
||||
0 => None,
|
||||
_ => Some(MsgId::new(entry.1)),
|
||||
};
|
||||
|
||||
let chat_id = ChatId::new(entry);
|
||||
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
|
||||
|
||||
if chat_id.is_archived_link() {
|
||||
@@ -72,8 +62,12 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
});
|
||||
}
|
||||
|
||||
let chat = Chat::load_from_db(ctx, chat_id).await?;
|
||||
let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat)).await?;
|
||||
let last_msgid = get_last_message_for_chat(ctx, chat_id).await?;
|
||||
|
||||
let chat = Chat::load_from_db(ctx, chat_id).await.context("chat")?;
|
||||
let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat))
|
||||
.await
|
||||
.context("summary")?;
|
||||
|
||||
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
|
||||
let summary_text2 = summary.text.to_owned();
|
||||
@@ -101,7 +95,8 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
let contact = chat_contacts.get(0);
|
||||
let was_seen_recently = match contact {
|
||||
Some(contact) => Contact::load_from_db(ctx, *contact)
|
||||
.await?
|
||||
.await
|
||||
.context("contact")?
|
||||
.was_seen_recently(),
|
||||
None => false,
|
||||
};
|
||||
|
||||
29
deltachat-jsonrpc/src/api/types/http.rs
Normal file
29
deltachat-jsonrpc/src/api/types/http.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use deltachat::net::HttpResponse as CoreHttpResponse;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
pub struct HttpResponse {
|
||||
/// base64-encoded response body.
|
||||
blob: String,
|
||||
|
||||
/// MIME type, e.g. "text/plain" or "text/html".
|
||||
mimetype: Option<String>,
|
||||
|
||||
/// Encoding, e.g. "utf-8".
|
||||
encoding: Option<String>,
|
||||
}
|
||||
|
||||
impl From<CoreHttpResponse> for HttpResponse {
|
||||
fn from(response: CoreHttpResponse) -> Self {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob);
|
||||
let mimetype = response.mimetype;
|
||||
let encoding = response.encoding;
|
||||
HttpResponse {
|
||||
blob,
|
||||
mimetype,
|
||||
encoding,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ pub mod account;
|
||||
pub mod chat;
|
||||
pub mod chat_list;
|
||||
pub mod contact;
|
||||
pub mod http;
|
||||
pub mod location;
|
||||
pub mod message;
|
||||
pub mod provider_info;
|
||||
|
||||
@@ -32,6 +32,9 @@ pub enum QrObject {
|
||||
Account {
|
||||
domain: String,
|
||||
},
|
||||
Backup {
|
||||
ticket: String,
|
||||
},
|
||||
WebrtcInstance {
|
||||
domain: String,
|
||||
instance_pattern: String,
|
||||
@@ -126,6 +129,9 @@ impl From<Qr> for QrObject {
|
||||
}
|
||||
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
|
||||
Qr::Account { domain } => QrObject::Account { domain },
|
||||
Qr::Backup { ticket } => QrObject::Backup {
|
||||
ticket: ticket.to_string(),
|
||||
},
|
||||
Qr::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
|
||||
@@ -6,7 +6,6 @@ use yerpc::axum::handle_ws_rpc;
|
||||
use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
mod api;
|
||||
use api::events::event_to_json_rpc_notification;
|
||||
use api::{Accounts, CommandApi};
|
||||
|
||||
const DEFAULT_PORT: u16 = 20808;
|
||||
@@ -44,12 +43,5 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
|
||||
let (client, out_receiver) = RpcClient::new();
|
||||
let session = RpcSession::new(client.clone(), api.clone());
|
||||
tokio::spawn(async move {
|
||||
let events = api.accounts.read().await.get_event_emitter();
|
||||
while let Some(event) = events.recv().await {
|
||||
let event = event_to_json_rpc_notification(event);
|
||||
client.send_notification("event", Some(event)).await.ok();
|
||||
}
|
||||
});
|
||||
handle_ws_rpc(ws, out_receiver, session).await
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ async function run() {
|
||||
null,
|
||||
null
|
||||
);
|
||||
for (const [chatId, _messageId] of chats) {
|
||||
for (const chatId of chats) {
|
||||
const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
|
||||
write($main, `<h3>${chat.name}</h3>`);
|
||||
const messageIds = await client.rpc.getMessageIds(
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.111.0"
|
||||
"version": "1.114.0"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as T from "../generated/types.js";
|
||||
import * as RPC from "../generated/jsonrpc.js";
|
||||
import { RawClient } from "../generated/client.js";
|
||||
import { Event } from "../generated/events.js";
|
||||
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
||||
import { TinyEmitter } from "@deltachat/tiny-emitter";
|
||||
|
||||
@@ -36,27 +35,33 @@ export class BaseDeltaChat<
|
||||
rpc: RawClient;
|
||||
account?: T.Account;
|
||||
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
||||
constructor(public transport: Transport) {
|
||||
|
||||
//@ts-ignore
|
||||
private eventTask: Promise<void>;
|
||||
|
||||
constructor(public transport: Transport, startEventLoop: boolean) {
|
||||
super();
|
||||
this.rpc = new RawClient(this.transport);
|
||||
this.transport.on("request", (request: Request) => {
|
||||
const method = request.method;
|
||||
if (method === "event") {
|
||||
const event = request.params! as DCWireEvent<Event>;
|
||||
//@ts-ignore
|
||||
this.emit(event.event.type, event.contextId, event.event as any);
|
||||
this.emit("ALL", event.contextId, event.event as any);
|
||||
if (startEventLoop) {
|
||||
this.eventTask = this.eventLoop();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.contextEmitters[event.contextId]) {
|
||||
this.contextEmitters[event.contextId].emit(
|
||||
event.event.type,
|
||||
//@ts-ignore
|
||||
event.event as any
|
||||
);
|
||||
this.contextEmitters[event.contextId].emit("ALL", event.event);
|
||||
}
|
||||
async eventLoop(): Promise<void> {
|
||||
while (true) {
|
||||
const event = await this.rpc.getNextEvent();
|
||||
this.emit(event.event.type, event.context_id, event.event as any);
|
||||
this.emit("ALL", event.context_id, event.event as any);
|
||||
|
||||
if (this.contextEmitters[event.context_id]) {
|
||||
this.contextEmitters[event.context_id].emit(
|
||||
event.event.type,
|
||||
//@ts-ignore
|
||||
event.event as any
|
||||
);
|
||||
this.contextEmitters[event.context_id].emit("ALL", event.event as any);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async listAccounts(): Promise<T.Account[]> {
|
||||
@@ -75,10 +80,12 @@ export class BaseDeltaChat<
|
||||
|
||||
export type Opts = {
|
||||
url: string;
|
||||
startEventLoop: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_OPTS: Opts = {
|
||||
url: "ws://localhost:20808/ws",
|
||||
startEventLoop: true,
|
||||
};
|
||||
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
||||
opts: Opts;
|
||||
@@ -86,20 +93,24 @@ export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
||||
this.transport.close();
|
||||
}
|
||||
constructor(opts?: Opts | string) {
|
||||
if (typeof opts === "string") opts = { url: opts };
|
||||
if (opts) opts = { ...DEFAULT_OPTS, ...opts };
|
||||
else opts = { ...DEFAULT_OPTS };
|
||||
if (typeof opts === "string") {
|
||||
opts = { ...DEFAULT_OPTS, url: opts };
|
||||
} else if (opts) {
|
||||
opts = { ...DEFAULT_OPTS, ...opts };
|
||||
} else {
|
||||
opts = { ...DEFAULT_OPTS };
|
||||
}
|
||||
const transport = new WebsocketTransport(opts.url);
|
||||
super(transport);
|
||||
super(transport, opts.startEventLoop);
|
||||
this.opts = opts;
|
||||
}
|
||||
}
|
||||
|
||||
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
||||
close() {}
|
||||
constructor(input: any, output: any) {
|
||||
constructor(input: any, output: any, startEventLoop: boolean) {
|
||||
const transport = new StdioTransport(input, output);
|
||||
super(transport);
|
||||
super(transport, startEventLoop);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export * as RPC from "../generated/jsonrpc.js";
|
||||
export * as T from "../generated/types.js";
|
||||
export * from "../generated/events.js";
|
||||
export { RawClient } from "../generated/client.js";
|
||||
export * from "./client.js";
|
||||
export * as yerpc from "yerpc";
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("basic tests", () => {
|
||||
|
||||
before(async () => {
|
||||
serverHandle = await startServer();
|
||||
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
|
||||
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
|
||||
// dc.on("ALL", (event) => {
|
||||
//console.log("event", event);
|
||||
// });
|
||||
|
||||
@@ -27,7 +27,7 @@ describe("online tests", function () {
|
||||
this.skip();
|
||||
}
|
||||
serverHandle = await startServer();
|
||||
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
|
||||
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
|
||||
|
||||
dc.on("ALL", (contextId, { type }) => {
|
||||
if (type !== "Info") console.log(contextId, type);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.111.0"
|
||||
version = "1.114.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
|
||||
@@ -8,10 +8,10 @@ edition = "2021"
|
||||
ansi_term = "0.12.1"
|
||||
anyhow = "1"
|
||||
deltachat = { path = "..", features = ["internals"]}
|
||||
dirs = "4"
|
||||
dirs = "5"
|
||||
log = "0.4.16"
|
||||
pretty_env_logger = "0.4"
|
||||
rusqlite = "0.28"
|
||||
rusqlite = "0.29"
|
||||
rustyline = "11"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
|
||||
|
||||
@@ -336,6 +336,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
has-backup\n\
|
||||
export-backup\n\
|
||||
import-backup <backup-file>\n\
|
||||
send-backup\n\
|
||||
receive-backup <qr>\n\
|
||||
export-keys\n\
|
||||
import-keys\n\
|
||||
export-setup\n\
|
||||
@@ -486,6 +488,17 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
"send-backup" => {
|
||||
let provider = BackupProvider::prepare(&context).await?;
|
||||
let qr = provider.qr();
|
||||
println!("QR code: {}", format_backup(&qr)?);
|
||||
provider.await?;
|
||||
}
|
||||
"receive-backup" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr> is missing.");
|
||||
let qr = check_qr(&context, arg1).await?;
|
||||
deltachat::imex::get_backup(&context, qr).await?;
|
||||
}
|
||||
"export-keys" => {
|
||||
let dir = dirs::home_dir().unwrap_or_default();
|
||||
imex(&context, ImexMode::ExportSelfKeys, dir.as_ref(), None).await?;
|
||||
@@ -550,7 +563,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
context.maybe_network().await;
|
||||
}
|
||||
"housekeeping" => {
|
||||
sql::housekeeping(&context).await.ok_or_log(&context);
|
||||
sql::housekeeping(&context).await.log_err(&context).ok();
|
||||
}
|
||||
"listchats" | "listarchived" | "chats" => {
|
||||
let listflags = if arg0 == "listarchived" {
|
||||
|
||||
@@ -152,13 +152,15 @@ impl Completer for DcHelper {
|
||||
}
|
||||
}
|
||||
|
||||
const IMEX_COMMANDS: [&str; 12] = [
|
||||
const IMEX_COMMANDS: [&str; 14] = [
|
||||
"initiate-key-transfer",
|
||||
"get-setupcodebegin",
|
||||
"continue-key-transfer",
|
||||
"has-backup",
|
||||
"export-backup",
|
||||
"import-backup",
|
||||
"send-backup",
|
||||
"receive-backup",
|
||||
"export-keys",
|
||||
"import-keys",
|
||||
"export-setup",
|
||||
|
||||
@@ -5,9 +5,23 @@ and provides asynchronous interface to it.
|
||||
|
||||
## Getting started
|
||||
|
||||
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
||||
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`
|
||||
or download a prebuilt release.
|
||||
Install it anywhere in your `PATH`.
|
||||
|
||||
[Create a virtual environment](https://docs.python.org/3/library/venv.html)
|
||||
if you don't have one already and activate it.
|
||||
```
|
||||
$ python -m venv env
|
||||
$ . env/bin/activate
|
||||
```
|
||||
|
||||
Install `deltachat-rpc-client` from source:
|
||||
```
|
||||
$ cd deltachat-rpc-client
|
||||
$ pip install .
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, EventType, Rpc
|
||||
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -30,9 +30,9 @@ async def main():
|
||||
await deltachat.start_io()
|
||||
|
||||
async def process_messages():
|
||||
for message in await account.get_fresh_messages_in_arrival_order():
|
||||
for message in await account.get_next_messages():
|
||||
snapshot = await message.get_snapshot()
|
||||
if not snapshot.is_bot and not snapshot.is_info:
|
||||
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
|
||||
await snapshot.chat.send_text(snapshot.text)
|
||||
await snapshot.message.mark_seen()
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ deltachat_rpc_client = [
|
||||
[project.entry-points.pytest11]
|
||||
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
root = ".."
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from ._utils import AttrDict, run_bot_cli, run_client_cli
|
||||
from .account import Account
|
||||
from .chat import Chat
|
||||
from .client import Bot, Client
|
||||
from .const import EventType
|
||||
from .const import EventType, SpecialContactId
|
||||
from .contact import Contact
|
||||
from .deltachat import DeltaChat
|
||||
from .message import Message
|
||||
@@ -19,6 +19,7 @@ __all__ = [
|
||||
"DeltaChat",
|
||||
"EventType",
|
||||
"Message",
|
||||
"SpecialContactId",
|
||||
"Rpc",
|
||||
"run_bot_cli",
|
||||
"run_client_cli",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
||||
from warnings import warn
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .chat import Chat
|
||||
@@ -176,7 +177,7 @@ class Account:
|
||||
|
||||
entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
|
||||
if not snapshot:
|
||||
return [Chat(self, entry[0]) for entry in entries]
|
||||
return [Chat(self, entry) for entry in entries]
|
||||
|
||||
items = await self._rpc.get_chatlist_items_by_entries(self.id, entries)
|
||||
chats = []
|
||||
@@ -239,7 +240,22 @@ class Account:
|
||||
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
async def get_next_messages(self) -> List[Message]:
|
||||
"""Return a list of next messages."""
|
||||
next_msg_ids = await self._rpc.get_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
async def wait_next_messages(self) -> List[Message]:
|
||||
"""Wait for new messages and return a list of them."""
|
||||
next_msg_ids = await self._rpc.wait_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||
warn(
|
||||
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
@@ -105,6 +105,10 @@ class Chat:
|
||||
info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
|
||||
return AttrDict(chat=self, **info)
|
||||
|
||||
async def can_send(self) -> bool:
|
||||
"""Return true if messages can be sent to the chat."""
|
||||
return await self._rpc.can_send(self.account.id, self.id)
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
text: Optional[str] = None,
|
||||
@@ -126,7 +130,7 @@ class Chat:
|
||||
"file": file,
|
||||
"location": location,
|
||||
"overrideSenderName": override_sender_name,
|
||||
"quotedMsg": quoted_msg,
|
||||
"quotedMessageId": quoted_msg,
|
||||
}
|
||||
msg_id = await self._rpc.send_msg(self.account.id, self.id, draft)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
@@ -20,7 +20,7 @@ from ._utils import (
|
||||
parse_system_image_changed,
|
||||
parse_system_title_changed,
|
||||
)
|
||||
from .const import COMMAND_PREFIX, EventType, SystemMessageType
|
||||
from .const import COMMAND_PREFIX, EventType, SpecialContactId, SystemMessageType
|
||||
from .events import (
|
||||
EventFilter,
|
||||
GroupImageChanged,
|
||||
@@ -189,9 +189,10 @@ class Client:
|
||||
|
||||
async def _process_messages(self) -> None:
|
||||
if self._should_process_messages:
|
||||
for message in await self.account.get_fresh_messages_in_arrival_order():
|
||||
for message in await self.account.get_next_messages():
|
||||
snapshot = await message.get_snapshot()
|
||||
await self._on_new_msg(snapshot)
|
||||
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]:
|
||||
await self._on_new_msg(snapshot)
|
||||
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
|
||||
await self._handle_info_msg(snapshot)
|
||||
await snapshot.message.mark_seen()
|
||||
|
||||
@@ -31,6 +31,7 @@ class EventType(str, Enum):
|
||||
SMTP_MESSAGE_SENT = "SmtpMessageSent"
|
||||
IMAP_MESSAGE_DELETED = "ImapMessageDeleted"
|
||||
IMAP_MESSAGE_MOVED = "ImapMessageMoved"
|
||||
IMAP_INBOX_IDLE = "ImapInboxIdle"
|
||||
NEW_BLOB_FILE = "NewBlobFile"
|
||||
DELETED_BLOB_FILE = "DeletedBlobFile"
|
||||
WARNING = "Warning"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Union
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .contact import Contact
|
||||
@@ -35,6 +35,13 @@ class Message:
|
||||
snapshot["message"] = self
|
||||
return snapshot
|
||||
|
||||
async def get_reactions(self) -> Optional[AttrDict]:
|
||||
"""Get message reactions."""
|
||||
reactions = await self._rpc.get_message_reactions(self.account.id, self.id)
|
||||
if reactions:
|
||||
return AttrDict(reactions)
|
||||
return None
|
||||
|
||||
async def mark_seen(self) -> None:
|
||||
"""Mark the message as seen."""
|
||||
await self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
|
||||
@@ -55,6 +55,11 @@ class ACFactory:
|
||||
async def get_online_account(self) -> Account:
|
||||
account = await self.new_configured_account()
|
||||
await account.start_io()
|
||||
while True:
|
||||
event = await account.wait_for_event()
|
||||
print(event)
|
||||
if event.type == EventType.IMAP_INBOX_IDLE:
|
||||
break
|
||||
return account
|
||||
|
||||
async def get_online_accounts(self, num: int) -> List[Account]:
|
||||
|
||||
@@ -23,7 +23,9 @@ class Rpc:
|
||||
self.event_queues: Dict[int, asyncio.Queue]
|
||||
# Map from request ID to `asyncio.Future` returning the response.
|
||||
self.request_events: Dict[int, asyncio.Future]
|
||||
self.closing: bool
|
||||
self.reader_task: asyncio.Task
|
||||
self.events_task: asyncio.Task
|
||||
|
||||
async def start(self) -> None:
|
||||
self.process = await asyncio.create_subprocess_exec(
|
||||
@@ -35,10 +37,15 @@ class Rpc:
|
||||
self.id = 0
|
||||
self.event_queues = {}
|
||||
self.request_events = {}
|
||||
self.closing = False
|
||||
self.reader_task = asyncio.create_task(self.reader_loop())
|
||||
self.events_task = asyncio.create_task(self.events_loop())
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Terminate RPC server process and wait until the reader loop finishes."""
|
||||
self.closing = True
|
||||
await self.stop_io_for_all_accounts()
|
||||
await self.events_task
|
||||
self.process.terminate()
|
||||
await self.reader_task
|
||||
|
||||
@@ -58,21 +65,28 @@ class Rpc:
|
||||
if "id" in response:
|
||||
fut = self.request_events.pop(response["id"])
|
||||
fut.set_result(response)
|
||||
elif response["method"] == "event":
|
||||
# An event notification.
|
||||
params = response["params"]
|
||||
account_id = params["contextId"]
|
||||
if account_id not in self.event_queues:
|
||||
self.event_queues[account_id] = asyncio.Queue()
|
||||
await self.event_queues[account_id].put(params["event"])
|
||||
else:
|
||||
print(response)
|
||||
|
||||
async def get_queue(self, account_id: int) -> asyncio.Queue:
|
||||
if account_id not in self.event_queues:
|
||||
self.event_queues[account_id] = asyncio.Queue()
|
||||
return self.event_queues[account_id]
|
||||
|
||||
async def events_loop(self) -> None:
|
||||
"""Requests new events and distributes them between queues."""
|
||||
while True:
|
||||
if self.closing:
|
||||
return
|
||||
event = await self.get_next_event()
|
||||
account_id = event["context_id"]
|
||||
queue = await self.get_queue(account_id)
|
||||
await queue.put(event["event"])
|
||||
|
||||
async def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||
"""Waits for the next event from the given account and returns it."""
|
||||
if account_id in self.event_queues:
|
||||
return await self.event_queues[account_id].get()
|
||||
return None
|
||||
queue = await self.get_queue(account_id)
|
||||
return await queue.get()
|
||||
|
||||
def __getattr__(self, attr: str):
|
||||
async def method(*args, **kwargs) -> Any:
|
||||
|
||||
@@ -98,8 +98,8 @@ async def test_account(acfactory) -> None:
|
||||
assert await alice.get_chatlist()
|
||||
assert await alice.get_chatlist(snapshot=True)
|
||||
assert await alice.get_qr_code()
|
||||
await alice.get_fresh_messages()
|
||||
await alice.get_fresh_messages_in_arrival_order()
|
||||
assert await alice.get_fresh_messages()
|
||||
assert await alice.get_next_messages()
|
||||
|
||||
group = await alice.create_group("test group")
|
||||
await group.add_contact(alice_contact_bob)
|
||||
@@ -147,7 +147,9 @@ async def test_chat(acfactory) -> None:
|
||||
assert alice_chat_bob != bob_chat_alice
|
||||
assert repr(alice_chat_bob)
|
||||
await alice_chat_bob.delete()
|
||||
assert not await bob_chat_alice.can_send()
|
||||
await bob_chat_alice.accept()
|
||||
assert await bob_chat_alice.can_send()
|
||||
await bob_chat_alice.block()
|
||||
bob_chat_alice = await snapshot.sender.create_chat()
|
||||
await bob_chat_alice.mute()
|
||||
@@ -237,6 +239,10 @@ async def test_message(acfactory) -> None:
|
||||
|
||||
await message.mark_seen()
|
||||
await message.send_reaction("😎")
|
||||
reactions = await message.get_reactions()
|
||||
assert reactions
|
||||
snapshot = await message.get_snapshot()
|
||||
assert reactions == snapshot.reactions
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
@@ -303,3 +309,29 @@ async def test_bot(acfactory) -> None:
|
||||
await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
||||
mock.hook.assert_called_once_with(event.msg_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_wait_next_messages(acfactory) -> None:
|
||||
alice = await acfactory.new_configured_account()
|
||||
|
||||
# Create a bot account so it does not receive device messages in the beginning.
|
||||
bot = await acfactory.new_preconfigured_account()
|
||||
await bot.set_config("bot", "1")
|
||||
await bot.configure()
|
||||
|
||||
# There are no old messages and the call returns immediately.
|
||||
assert not await bot.wait_next_messages()
|
||||
|
||||
# Bot starts waiting for messages.
|
||||
next_messages_task = asyncio.create_task(bot.wait_next_messages())
|
||||
|
||||
bot_addr = await bot.get_config("addr")
|
||||
alice_contact_bot = await alice.create_contact(bot_addr, "Bob")
|
||||
alice_chat_bot = await alice_contact_bot.create_chat()
|
||||
await alice_chat_bot.send_text("Hello!")
|
||||
|
||||
next_messages = await next_messages_task
|
||||
assert len(next_messages) == 1
|
||||
snapshot = await next_messages[0].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
@@ -15,6 +15,7 @@ passenv =
|
||||
deps =
|
||||
pytest
|
||||
pytest-asyncio
|
||||
pytest-timeout
|
||||
aiohttp
|
||||
aiodns
|
||||
|
||||
@@ -25,5 +26,8 @@ deps =
|
||||
ruff
|
||||
black
|
||||
commands =
|
||||
black --check --diff src/ examples/ tests/
|
||||
black --quiet --check --diff src/ examples/ tests/
|
||||
ruff src/ examples/ tests/
|
||||
|
||||
[pytest]
|
||||
timeout = 60
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.111.0"
|
||||
version = "1.114.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
@@ -9,19 +9,18 @@ license = "MPL-2.0"
|
||||
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
||||
categories = ["cryptography", "std", "email"]
|
||||
|
||||
[[bin]]
|
||||
name = "deltachat-rpc-server"
|
||||
|
||||
[dependencies]
|
||||
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = "..", default-features = false }
|
||||
|
||||
anyhow = "1"
|
||||
env_logger = { version = "0.10.0" }
|
||||
futures-lite = "1.12.0"
|
||||
futures-lite = "1.13.0"
|
||||
log = "0.4"
|
||||
serde_json = "1.0.91"
|
||||
serde_json = "1.0.96"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.25.0", features = ["io-std"] }
|
||||
tokio = { version = "1.28.0", features = ["io-std"] }
|
||||
tokio-util = "0.7.8"
|
||||
yerpc = { version = "0.4.0", features = ["anyhow_expose"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
///! Delta Chat core RPC server.
|
||||
///!
|
||||
///! It speaks JSON Lines over stdio.
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_jsonrpc::api::events::event_to_json_rpc_notification;
|
||||
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
||||
use futures_lite::stream::StreamExt;
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
use tokio::task::JoinHandle;
|
||||
use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
|
||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
||||
log::info!("Starting with accounts directory `{}`.", path);
|
||||
let accounts = Accounts::new(PathBuf::from(&path)).await?;
|
||||
let events = accounts.get_event_emitter();
|
||||
|
||||
log::info!("Creating JSON-RPC API.");
|
||||
let state = CommandApi::new(accounts);
|
||||
|
||||
let (client, mut out_receiver) = RpcClient::new();
|
||||
let session = RpcSession::new(client.clone(), state);
|
||||
|
||||
// Events task converts core events to JSON-RPC notifications.
|
||||
let events_task: JoinHandle<Result<()>> = tokio::spawn(async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
let event = event_to_json_rpc_notification(event);
|
||||
client.send_notification("event", Some(event)).await?;
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// Send task prints JSON responses to stdout.
|
||||
let send_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
||||
while let Some(message) = out_receiver.next().await {
|
||||
let message = serde_json::to_string(&message)?;
|
||||
log::trace!("RPC send {}", message);
|
||||
println!("{message}");
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// Receiver task reads JSON requests from stdin.
|
||||
let recv_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
||||
let stdin = io::stdin();
|
||||
let mut lines = BufReader::new(stdin).lines();
|
||||
while let Some(message) = lines.next_line().await? {
|
||||
log::trace!("RPC recv {}", message);
|
||||
let session = session.clone();
|
||||
tokio::spawn(async move {
|
||||
session.handle_incoming(&message).await;
|
||||
});
|
||||
}
|
||||
log::info!("EOF reached on stdin");
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// Wait for the end of stdin.
|
||||
recv_task.await??;
|
||||
|
||||
// Shutdown the server.
|
||||
send_task.abort();
|
||||
events_task.abort();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
143
deltachat-rpc-server/src/main.rs
Normal file
143
deltachat-rpc-server/src/main.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use std::env;
|
||||
///! Delta Chat core RPC server.
|
||||
///!
|
||||
///! It speaks JSON Lines over stdio.
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use deltachat::constants::DC_VERSION_STR;
|
||||
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
||||
use futures_lite::stream::StreamExt;
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
use tokio::signal::unix as signal_unix;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() {
|
||||
let r = main_impl().await;
|
||||
// From tokio documentation:
|
||||
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
|
||||
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
|
||||
// until the user presses enter."
|
||||
std::process::exit(if r.is_ok() { 0 } else { 1 });
|
||||
}
|
||||
|
||||
async fn main_impl() -> Result<()> {
|
||||
let mut args = env::args_os();
|
||||
let _program_name = args.next().context("no command line arguments found")?;
|
||||
if let Some(first_arg) = args.next() {
|
||||
if first_arg.to_str() == Some("--version") {
|
||||
if let Some(arg) = args.next() {
|
||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||
}
|
||||
eprintln!("{}", &*DC_VERSION_STR);
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(anyhow!("Unrecognized option {:?}", first_arg));
|
||||
}
|
||||
}
|
||||
if let Some(arg) = args.next() {
|
||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||
}
|
||||
|
||||
// Install signal handlers early so that the shutdown is graceful starting from here.
|
||||
let _ctrl_c = tokio::signal::ctrl_c();
|
||||
#[cfg(target_family = "unix")]
|
||||
let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
|
||||
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
|
||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
||||
log::info!("Starting with accounts directory `{}`.", path);
|
||||
let accounts = Accounts::new(PathBuf::from(&path)).await?;
|
||||
|
||||
log::info!("Creating JSON-RPC API.");
|
||||
let accounts = Arc::new(RwLock::new(accounts));
|
||||
let state = CommandApi::from_arc(accounts.clone());
|
||||
|
||||
let (client, mut out_receiver) = RpcClient::new();
|
||||
let session = RpcSession::new(client.clone(), state.clone());
|
||||
let main_cancel = CancellationToken::new();
|
||||
|
||||
// Send task prints JSON responses to stdout.
|
||||
let cancel = main_cancel.clone();
|
||||
let send_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
||||
let _cancel_guard = cancel.clone().drop_guard();
|
||||
loop {
|
||||
let message = tokio::select! {
|
||||
_ = cancel.cancelled() => break,
|
||||
message = out_receiver.next() => match message {
|
||||
None => break,
|
||||
Some(message) => serde_json::to_string(&message)?,
|
||||
}
|
||||
};
|
||||
log::trace!("RPC send {}", message);
|
||||
println!("{message}");
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let cancel = main_cancel.clone();
|
||||
let sigterm_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
let _cancel_guard = cancel.clone().drop_guard();
|
||||
tokio::select! {
|
||||
_ = cancel.cancelled() => (),
|
||||
_ = sigterm.recv() => {
|
||||
log::info!("got SIGTERM");
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = cancel;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// Receiver task reads JSON requests from stdin.
|
||||
let cancel = main_cancel.clone();
|
||||
let recv_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
||||
let _cancel_guard = cancel.clone().drop_guard();
|
||||
let stdin = io::stdin();
|
||||
let mut lines = BufReader::new(stdin).lines();
|
||||
|
||||
loop {
|
||||
let message = tokio::select! {
|
||||
_ = cancel.cancelled() => break,
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
log::info!("got ctrl-c event");
|
||||
break;
|
||||
}
|
||||
message = lines.next_line() => match message? {
|
||||
None => {
|
||||
log::info!("EOF reached on stdin");
|
||||
break;
|
||||
}
|
||||
Some(message) => message,
|
||||
}
|
||||
};
|
||||
log::trace!("RPC recv {}", message);
|
||||
let session = session.clone();
|
||||
tokio::spawn(async move {
|
||||
session.handle_incoming(&message).await;
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
main_cancel.cancelled().await;
|
||||
accounts.read().await.stop_io().await;
|
||||
drop(accounts);
|
||||
drop(state);
|
||||
send_task.await??;
|
||||
sigterm_task.await??;
|
||||
recv_task.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -8,5 +8,5 @@ license = "MPL-2.0"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1"
|
||||
syn = "2"
|
||||
quote = "1"
|
||||
|
||||
82
deny.toml
82
deny.toml
@@ -11,28 +11,53 @@ ignore = [
|
||||
# Accept some duplicate versions, ideally we work towards this list
|
||||
# becoming empty. Adding versions forces us to revisit this at least
|
||||
# when upgrading.
|
||||
# Please keep this list alphabetically sorted.
|
||||
skip = [
|
||||
{ name = "windows-sys", version = "<0.45" },
|
||||
{ name = "wasi", version = "<0.11" },
|
||||
{ name = "version_check", version = "<0.9" },
|
||||
{ name = "uuid", version = "<1.3" },
|
||||
{ name = "sha2", version = "<0.10" },
|
||||
{ name = "rand_core", version = "<0.6" },
|
||||
{ name = "rand_chacha", version = "<0.3" },
|
||||
{ name = "rand", version = "<0.8" },
|
||||
{ name = "nom", version = "<7.1" },
|
||||
{ name = "idna", version = "<0.3" },
|
||||
{ name = "humantime", version = "<2.1" },
|
||||
{ name = "hermit-abi", version = "<0.3" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "env_logger", version = "<0.10" },
|
||||
{ name = "digest", version = "<0.10" },
|
||||
{ name = "darling_macro", version = "<0.14" },
|
||||
{ name = "darling_core", version = "<0.14" },
|
||||
{ name = "darling", version = "<0.14" },
|
||||
{ name = "block-buffer", version = "<0.10" },
|
||||
{ name = "base16ct", version = "0.1.1" },
|
||||
{ name = "base64", version = "<0.21" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "block-buffer", version = "<0.10" },
|
||||
{ name = "clap_lex", version = "0.2.4" },
|
||||
{ name = "clap", version = "3.2.23" },
|
||||
{ name = "convert_case", version = "0.4.0" },
|
||||
{ name = "curve25519-dalek", version = "3.2.0" },
|
||||
{ name = "darling_core", version = "<0.14" },
|
||||
{ name = "darling_macro", version = "<0.14" },
|
||||
{ name = "darling", version = "<0.14" },
|
||||
{ name = "der", version = "0.6.1" },
|
||||
{ name = "digest", version = "<0.10" },
|
||||
{ name = "ed25519-dalek", version = "1.0.1" },
|
||||
{ name = "ed25519", version = "1.5.3" },
|
||||
{ name = "env_logger", version = "<0.10" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "hermit-abi", version = "<0.3" },
|
||||
{ name = "humantime", version = "<2.1" },
|
||||
{ name = "idna", version = "<0.3" },
|
||||
{ name = "libm", version = "0.1.4" },
|
||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||
{ name = "pkcs8", version = "0.9.0" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "rand_chacha", version = "<0.3" },
|
||||
{ name = "rand_core", version = "<0.6" },
|
||||
{ name = "rand", version = "<0.8" },
|
||||
{ name = "redox_syscall", version = "0.2.16" },
|
||||
{ name = "sec1", version = "0.3.0" },
|
||||
{ name = "sha2", version = "<0.10" },
|
||||
{ name = "signature", version = "1.6.4" },
|
||||
{ name = "spin", version = "<0.9.6" },
|
||||
{ name = "spki", version = "0.6.0" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "wasi", version = "<0.11" },
|
||||
{ name = "windows_aarch64_gnullvm", version = "<0.48" },
|
||||
{ name = "windows_aarch64_msvc", version = "<0.48" },
|
||||
{ name = "windows_i686_gnu", version = "<0.48" },
|
||||
{ name = "windows_i686_msvc", version = "<0.48" },
|
||||
{ name = "windows-sys", version = "<0.48" },
|
||||
{ name = "windows-targets", version = "<0.48" },
|
||||
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
|
||||
{ name = "windows_x86_64_gnu", version = "<0.48" },
|
||||
{ name = "windows_x86_64_msvc", version = "<0.48" },
|
||||
]
|
||||
|
||||
|
||||
@@ -42,11 +67,21 @@ allow = [
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"CC0-1.0",
|
||||
"MIT",
|
||||
"BSL-1.0", # Boost Software License 1.0
|
||||
"Unicode-DFS-2016",
|
||||
"CC0-1.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"OpenSSL",
|
||||
"Unicode-DFS-2016",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[[licenses.clarify]]
|
||||
name = "ring"
|
||||
expression = "MIT AND ISC AND OpenSSL"
|
||||
license-files = [
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 },
|
||||
]
|
||||
|
||||
[sources.allow-org]
|
||||
@@ -54,4 +89,5 @@ allow = [
|
||||
github = [
|
||||
"async-email",
|
||||
"deltachat",
|
||||
"quinn-rs",
|
||||
]
|
||||
|
||||
1881
fuzz/Cargo.lock
generated
1881
fuzz/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,7 @@ module.exports = {
|
||||
DC_EVENT_ERROR: 400,
|
||||
DC_EVENT_ERROR_SELF_NOT_IN_GROUP: 410,
|
||||
DC_EVENT_IMAP_CONNECTED: 102,
|
||||
DC_EVENT_IMAP_INBOX_IDLE: 106,
|
||||
DC_EVENT_IMAP_MESSAGE_DELETED: 104,
|
||||
DC_EVENT_IMAP_MESSAGE_MOVED: 105,
|
||||
DC_EVENT_IMEX_FILE_WRITTEN: 2052,
|
||||
@@ -112,6 +113,7 @@ module.exports = {
|
||||
DC_QR_ADDR: 320,
|
||||
DC_QR_ASK_VERIFYCONTACT: 200,
|
||||
DC_QR_ASK_VERIFYGROUP: 202,
|
||||
DC_QR_BACKUP: 251,
|
||||
DC_QR_ERROR: 400,
|
||||
DC_QR_FPR_MISMATCH: 220,
|
||||
DC_QR_FPR_OK: 210,
|
||||
@@ -149,6 +151,8 @@ module.exports = {
|
||||
DC_STR_AEAP_EXPLANATION_AND_LINK: 123,
|
||||
DC_STR_ARCHIVEDCHATS: 40,
|
||||
DC_STR_AUDIO: 11,
|
||||
DC_STR_BACKUP_TRANSFER_MSG_BODY: 163,
|
||||
DC_STR_BACKUP_TRANSFER_QR: 162,
|
||||
DC_STR_BAD_TIME_MSG_BODY: 85,
|
||||
DC_STR_BROADCAST_LIST: 115,
|
||||
DC_STR_CANNOT_LOGIN: 60,
|
||||
|
||||
@@ -8,6 +8,7 @@ module.exports = {
|
||||
103: 'DC_EVENT_SMTP_MESSAGE_SENT',
|
||||
104: 'DC_EVENT_IMAP_MESSAGE_DELETED',
|
||||
105: 'DC_EVENT_IMAP_MESSAGE_MOVED',
|
||||
106: 'DC_EVENT_IMAP_INBOX_IDLE',
|
||||
150: 'DC_EVENT_NEW_BLOB_FILE',
|
||||
151: 'DC_EVENT_DELETED_BLOB_FILE',
|
||||
300: 'DC_EVENT_WARNING',
|
||||
|
||||
@@ -37,6 +37,7 @@ export enum C {
|
||||
DC_EVENT_ERROR = 400,
|
||||
DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410,
|
||||
DC_EVENT_IMAP_CONNECTED = 102,
|
||||
DC_EVENT_IMAP_INBOX_IDLE = 106,
|
||||
DC_EVENT_IMAP_MESSAGE_DELETED = 104,
|
||||
DC_EVENT_IMAP_MESSAGE_MOVED = 105,
|
||||
DC_EVENT_IMEX_FILE_WRITTEN = 2052,
|
||||
@@ -112,6 +113,7 @@ export enum C {
|
||||
DC_QR_ADDR = 320,
|
||||
DC_QR_ASK_VERIFYCONTACT = 200,
|
||||
DC_QR_ASK_VERIFYGROUP = 202,
|
||||
DC_QR_BACKUP = 251,
|
||||
DC_QR_ERROR = 400,
|
||||
DC_QR_FPR_MISMATCH = 220,
|
||||
DC_QR_FPR_OK = 210,
|
||||
@@ -149,6 +151,8 @@ export enum C {
|
||||
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
|
||||
DC_STR_ARCHIVEDCHATS = 40,
|
||||
DC_STR_AUDIO = 11,
|
||||
DC_STR_BACKUP_TRANSFER_MSG_BODY = 163,
|
||||
DC_STR_BACKUP_TRANSFER_QR = 162,
|
||||
DC_STR_BAD_TIME_MSG_BODY = 85,
|
||||
DC_STR_BROADCAST_LIST = 115,
|
||||
DC_STR_CANNOT_LOGIN = 60,
|
||||
@@ -289,6 +293,7 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
103: 'DC_EVENT_SMTP_MESSAGE_SENT',
|
||||
104: 'DC_EVENT_IMAP_MESSAGE_DELETED',
|
||||
105: 'DC_EVENT_IMAP_MESSAGE_MOVED',
|
||||
106: 'DC_EVENT_IMAP_INBOX_IDLE',
|
||||
150: 'DC_EVENT_NEW_BLOB_FILE',
|
||||
151: 'DC_EVENT_DELETED_BLOB_FILE',
|
||||
300: 'DC_EVENT_WARNING',
|
||||
|
||||
@@ -60,5 +60,5 @@
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.111.0"
|
||||
}
|
||||
"version": "1.114.0"
|
||||
}
|
||||
|
||||
@@ -12,17 +12,17 @@ a low-level Chat/Contact/Message API to user interfaces and bots.
|
||||
Installing pre-built packages (Linux-only)
|
||||
==========================================
|
||||
|
||||
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
|
||||
If you have a Linux system you may install the ``deltachat`` binary "wheel" packages
|
||||
without any "build-from-source" steps.
|
||||
Otherwise you need to `compile the Delta Chat bindings yourself`__.
|
||||
|
||||
__ sourceinstall_
|
||||
|
||||
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation.html>`_,
|
||||
then create a fresh Python virtual environment and activate it in your shell::
|
||||
We recommend to first create a fresh Python virtual environment
|
||||
and activate it in your shell::
|
||||
|
||||
virtualenv env # or: python -m venv
|
||||
source env/bin/activate
|
||||
python -m venv env
|
||||
source env/bin/activate
|
||||
|
||||
Afterwards, invoking ``python`` or ``pip install`` only
|
||||
modifies files in your ``env`` directory and leaves
|
||||
@@ -40,16 +40,14 @@ To verify it worked::
|
||||
Running tests
|
||||
=============
|
||||
|
||||
Recommended way to run tests is using `tox <https://tox.wiki>`_.
|
||||
After successful binding installation you can install tox
|
||||
and run the tests::
|
||||
Recommended way to run tests is using `scripts/run-python-test.sh`
|
||||
script provided in the core repository.
|
||||
|
||||
pip install tox
|
||||
tox -e py3
|
||||
|
||||
This will run all "offline" tests and skip all functional
|
||||
This script compiles the library in debug mode and runs the tests using `tox`_.
|
||||
By default it will run all "offline" tests and skip all functional
|
||||
end-to-end tests that require accounts on real e-mail servers.
|
||||
|
||||
.. _`tox`: https://tox.wiki
|
||||
.. _livetests:
|
||||
|
||||
Running "live" tests with temporary accounts
|
||||
@@ -61,13 +59,32 @@ Please feel free to contact us through a github issue or by e-mail and we'll sen
|
||||
|
||||
export DCC_NEW_TMP_EMAIL=<URL you got from us>
|
||||
|
||||
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server. These accounts exists only for one hour and then are removed completely.
|
||||
One hour is enough to invoke pytest and run all offline and online tests::
|
||||
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server.
|
||||
These accounts are removed automatically as they expire.
|
||||
After setting the variable, either rerun `scripts/run-python-test.sh`
|
||||
or run offline and online tests with `tox` directly::
|
||||
|
||||
tox -e py3
|
||||
tox -e py
|
||||
|
||||
Each test run creates new accounts.
|
||||
|
||||
Developing the bindings
|
||||
-----------------------
|
||||
|
||||
If you want to develop or debug the bindings,
|
||||
you can create a testing development environment using `tox`::
|
||||
|
||||
tox -c python --devenv env
|
||||
. env/bin/activate
|
||||
|
||||
Inside this environment the bindings are installed
|
||||
in editable mode (as if installed with `python -m pip install -e`)
|
||||
together with the testing dependencies like `pytest` and its plugins.
|
||||
|
||||
You can then edit the source code in the development tree
|
||||
and quickly run `pytest` manually without waiting for `tox`
|
||||
to recreating the virtual environment each time.
|
||||
|
||||
.. _sourceinstall:
|
||||
|
||||
Installing bindings from source
|
||||
@@ -89,20 +106,34 @@ To install the Delta Chat Python bindings make sure you have Python3 installed.
|
||||
E.g. on Debian-based systems `apt install python3 python3-pip
|
||||
python3-venv` should give you a usable python installation.
|
||||
|
||||
Ensure you are in the deltachat-core-rust/python directory, create the
|
||||
virtual environment with dependencies using tox
|
||||
and activate it in your shell::
|
||||
First, build the core library::
|
||||
|
||||
cd python
|
||||
tox --devenv env
|
||||
cargo build --release -p deltachat_ffi --features jsonrpc
|
||||
|
||||
`jsonrpc` feature is required even if not used by the bindings
|
||||
because `deltachat.h` includes JSON-RPC functions unconditionally.
|
||||
|
||||
Create the virtual environment and activate it:
|
||||
|
||||
python -m venv env
|
||||
source env/bin/activate
|
||||
|
||||
You should now be able to build the python bindings using the supplied script::
|
||||
Build and install the bindings:
|
||||
|
||||
python3 install_python_bindings.py
|
||||
export DCC_RS_DEV="$PWD"
|
||||
export DCC_RS_TARGET=release
|
||||
python -m pip install ./python
|
||||
|
||||
The core compilation and bindings building might take a while,
|
||||
depending on the speed of your machine.
|
||||
`DCC_RS_DEV` environment variable specifies the location of
|
||||
the core development tree. If this variable is not set,
|
||||
`libdeltachat` library and `deltachat.h` header are expected
|
||||
to be installed system-wide.
|
||||
|
||||
When `DCC_RS_DEV` is set, `DCC_RS_TARGET` specifies
|
||||
the build profile name to look up the artifacts
|
||||
in the target directory.
|
||||
In this case setting it can be skipped because
|
||||
`DCC_RS_TARGET=release` is the default.
|
||||
|
||||
Building manylinux based wheels
|
||||
===============================
|
||||
|
||||
@@ -24,6 +24,8 @@ def test_echo_quit_plugin(acfactory, lp):
|
||||
lp.sec("creating a temp account to contact the bot")
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
botproc.await_resync()
|
||||
|
||||
lp.sec("sending a message to the bot")
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
bot_chat = bot_contact.create_chat()
|
||||
@@ -40,7 +42,7 @@ def test_echo_quit_plugin(acfactory, lp):
|
||||
|
||||
def test_group_tracking_plugin(acfactory, lp):
|
||||
lp.sec("creating one group-tracking bot and two temp accounts")
|
||||
botproc = acfactory.run_bot_process(group_tracking, ffi=False)
|
||||
botproc = acfactory.run_bot_process(group_tracking)
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -52,6 +54,8 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
ac1.add_account_plugin(FFIEventLogger(ac1))
|
||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||
|
||||
botproc.await_resync()
|
||||
|
||||
lp.sec("creating bot test group with bot")
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
ch = ac1.create_group_chat("bot test group")
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
setup a python binding development in-place install with cargo debug symbols.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
target = os.environ.get("DCC_RS_TARGET")
|
||||
if target is None:
|
||||
os.environ["DCC_RS_TARGET"] = target = "debug"
|
||||
if "DCC_RS_DEV" not in os.environ:
|
||||
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
os.environ["DCC_RS_DEV"] = dn
|
||||
|
||||
cmd = ["cargo", "build", "-p", "deltachat_ffi", "--features", "jsonrpc"]
|
||||
|
||||
if target == "release":
|
||||
os.environ["CARGO_PROFILE_RELEASE_LTO"] = "on"
|
||||
cmd.append("--release")
|
||||
|
||||
print("running:", " ".join(cmd))
|
||||
subprocess.check_call(cmd)
|
||||
subprocess.check_call("rm -rf build/ src/deltachat/*.so src/deltachat/*.dylib src/deltachat/*.dll", shell=True)
|
||||
|
||||
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "-e", "."])
|
||||
@@ -376,6 +376,22 @@ 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()
|
||||
|
||||
@@ -190,7 +190,7 @@ class FFIEventTracker:
|
||||
- ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message
|
||||
- therefore no DC_EVENT_INCOMING_MSG is sent
|
||||
"""
|
||||
self.get_info_contains("INBOX: Idle entering")
|
||||
self.get_matching("DC_EVENT_IMAP_INBOX_IDLE")
|
||||
|
||||
def wait_next_incoming_message(self):
|
||||
"""wait for and return next incoming message."""
|
||||
|
||||
@@ -344,6 +344,16 @@ class Message:
|
||||
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
||||
return Contact(self.account, contact_id)
|
||||
|
||||
def is_from_self(self):
|
||||
"""Return true if the message is sent by self."""
|
||||
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
||||
return contact_id == const.DC_CONTACT_ID_SELF
|
||||
|
||||
def is_from_device(self):
|
||||
"""Return true if the message is sent by the device."""
|
||||
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
||||
return contact_id == const.DC_CONTACT_ID_DEVICE
|
||||
|
||||
#
|
||||
# Message State query methods
|
||||
#
|
||||
|
||||
@@ -676,6 +676,13 @@ class BotProcess:
|
||||
print("+++IGN:", line)
|
||||
ignored.append(line)
|
||||
|
||||
def await_resync(self):
|
||||
self.fnmatch_lines(
|
||||
"""
|
||||
*Resync: collected * message IDs in folder INBOX*
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def tmp_db_path(tmpdir):
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
assert len(sys.argv) == 2
|
||||
workspacedir = sys.argv[1]
|
||||
for relpath in os.listdir(workspacedir):
|
||||
if relpath.startswith("deltachat"):
|
||||
p = os.path.join(workspacedir, relpath)
|
||||
subprocess.check_call(
|
||||
[
|
||||
"auditwheel",
|
||||
"repair",
|
||||
p,
|
||||
"-w",
|
||||
workspacedir,
|
||||
],
|
||||
)
|
||||
@@ -220,16 +220,16 @@ def test_fetch_existing(acfactory, lp, mvbox_move):
|
||||
acfactory.bring_accounts_online()
|
||||
assert_folders_configured(ac1)
|
||||
|
||||
assert ac1.direct_imap.select_config_folder("mvbox" if mvbox_move else "inbox")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
chat.send_text("message text")
|
||||
assert_folders_configured(ac1)
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
chat.send_text("message text")
|
||||
|
||||
lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen")
|
||||
assert idle1.wait_for_seen()
|
||||
lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen")
|
||||
if mvbox_move:
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
|
||||
else:
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
assert_folders_configured(ac1)
|
||||
|
||||
lp.sec("create a cloned ac1 and fetch contact history during configure")
|
||||
@@ -271,12 +271,12 @@ def test_fetch_existing_msgs_group_and_single(acfactory, lp):
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
ac1_ac2_chat.send_text("outgoing, encrypted direct message, creating a chat")
|
||||
# wait until the bcc_self message arrives
|
||||
assert idle1.wait_for_seen()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
ac1_ac2_chat.send_text("outgoing, encrypted direct message, creating a chat")
|
||||
|
||||
# wait until the bcc_self message arrives
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
|
||||
lp.sec("Clone online account and let it fetch the existing messages")
|
||||
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
|
||||
@@ -44,21 +44,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._evtracker.wait_next_incoming_message()
|
||||
msg_in = ac2.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._evtracker.wait_next_incoming_message()
|
||||
msg2_in = ac1.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._evtracker.wait_next_incoming_message()
|
||||
msg3_in = ac2.wait_next_incoming_message()
|
||||
assert msg3_in.text == "message3"
|
||||
assert msg3_in.is_encrypted()
|
||||
|
||||
@@ -134,22 +134,27 @@ def test_one_account_send_bcc_setting(acfactory, lp):
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
msg_out = chat.send_text("message2")
|
||||
msg_out = chat.send_text("message2")
|
||||
|
||||
# wait for send out (BCC)
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
# wait for send out (BCC)
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
|
||||
# now make sure we are sending message to ourselves too
|
||||
assert self_addr in ev.data2
|
||||
assert other_addr in ev.data2
|
||||
assert idle1.wait_for_seen()
|
||||
|
||||
# Second client receives only second message, but not the first
|
||||
# Second client receives only second message, but not the first.
|
||||
ev_msg = ac1_clone._evtracker.wait_next_messages_changed()
|
||||
assert ev_msg.text == msg_out.text
|
||||
|
||||
# now make sure we are sending message to ourselves too
|
||||
assert self_addr in ev.data2
|
||||
assert other_addr in ev.data2
|
||||
|
||||
# BCC-self messages are marked as seen by the sender device.
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
|
||||
# Check that the message is marked as seen on IMAP.
|
||||
ac1.direct_imap.select_folder("Inbox")
|
||||
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_send_file_twice_unicode_filename_mangling(tmpdir, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
@@ -314,6 +319,9 @@ def test_webxdc_message(acfactory, data, lp):
|
||||
assert msg2.text == "message1"
|
||||
assert msg2.is_webxdc()
|
||||
assert msg2.filename
|
||||
ac2._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
ac2.direct_imap.select_folder("Inbox")
|
||||
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_mvbox_sentbox_threads(acfactory, lp):
|
||||
@@ -513,22 +521,22 @@ def test_send_and_receive_message_markseen(acfactory, lp):
|
||||
msg4 = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("mark messages as seen on ac2, wait for changes on ac1")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
with ac2.direct_imap.idle() as idle2:
|
||||
ac2.mark_seen_messages([msg2, msg4])
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
assert msg2.chat.id == msg4.chat.id
|
||||
assert ev.data1 == msg2.chat.id
|
||||
assert ev.data2 == 0
|
||||
idle2.wait_for_seen()
|
||||
ac2.mark_seen_messages([msg2, msg4])
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
assert msg2.chat.id == msg4.chat.id
|
||||
assert ev.data1 == msg2.chat.id
|
||||
assert ev.data2 == 0
|
||||
ac2._evtracker.get_info_contains("Marked messages .* in folder INBOX as seen.")
|
||||
|
||||
lp.step("1")
|
||||
for _i in range(2):
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
|
||||
assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
|
||||
lp.step("2")
|
||||
idle1.wait_for_seen() # Check that ac1 marks the read receipt as read
|
||||
lp.step("1")
|
||||
for _i in range(2):
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
|
||||
assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
|
||||
lp.step("2")
|
||||
|
||||
# Check that ac1 marks the read receipt as read.
|
||||
ac1._evtracker.get_info_contains("Marked messages .* in folder INBOX as seen.")
|
||||
|
||||
assert msg1.is_out_mdn_received()
|
||||
assert msg3.is_out_mdn_received()
|
||||
@@ -559,7 +567,8 @@ def test_moved_markseen(acfactory):
|
||||
|
||||
with ac2.direct_imap.idle() as idle2:
|
||||
ac2.start_io()
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
|
||||
# Accept the contact request.
|
||||
msg.chat.accept()
|
||||
@@ -613,18 +622,24 @@ def test_markseen_message_and_mdn(acfactory, mvbox_move):
|
||||
# Do not send BCC to self, we only want to test MDN on ac1.
|
||||
ac1.set_config("bcc_self", "0")
|
||||
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
ac2.mark_seen_messages([msg])
|
||||
|
||||
folder = "mvbox" if mvbox_move else "inbox"
|
||||
for ac in [ac1, ac2]:
|
||||
if mvbox_move:
|
||||
ac._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
|
||||
else:
|
||||
ac._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
ac1.direct_imap.select_config_folder(folder)
|
||||
ac2.direct_imap.select_config_folder(folder)
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
with ac2.direct_imap.idle() as idle2:
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
ac2.mark_seen_messages([msg])
|
||||
|
||||
idle2.wait_for_seen() # Check original message is marked as seen
|
||||
idle1.wait_for_seen() # Check that the mdn is marked as seen
|
||||
# Check that the mdn is marked as seen
|
||||
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
# Check original message is marked as seen
|
||||
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_reply_privately(acfactory):
|
||||
@@ -678,23 +693,24 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
|
||||
ac1.direct_imap.select_config_folder("mvbox")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
lp.sec("ac2: mark incoming message as seen")
|
||||
ac2.mark_seen_messages([msg])
|
||||
lp.sec("ac2: mark incoming message as seen")
|
||||
ac2.mark_seen_messages([msg])
|
||||
|
||||
lp.sec("ac1: waiting for incoming activity")
|
||||
# MDN should be moved even though MDNs are already disabled
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
lp.sec("ac1: waiting for incoming activity")
|
||||
# MDN should be moved even though MDNs are already disabled
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
assert len(chat.get_messages()) == 1
|
||||
assert len(chat.get_messages()) == 1
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
assert idle1.wait_for_seen()
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
ac1._evtracker.get_info_contains("Marked messages 1 in folder DeltaChat as seen.")
|
||||
|
||||
# MDN is received even though MDNs are already disabled
|
||||
assert msg_out.is_out_mdn_received()
|
||||
|
||||
ac1.direct_imap.select_config_folder("mvbox")
|
||||
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -33,18 +33,6 @@ passenv =
|
||||
CARGO_TARGET_DIR
|
||||
RUSTC_WRAPPER
|
||||
|
||||
[testenv:auditwheels]
|
||||
skipsdist = True
|
||||
deps = auditwheel
|
||||
passenv =
|
||||
DCC_RS_DEV
|
||||
DCC_RS_TARGET
|
||||
AUDITWHEEL_ARCH
|
||||
AUDITWHEEL_PLAT
|
||||
AUDITWHEEL_POLICY
|
||||
commands =
|
||||
python tests/auditwheels.py {toxworkdir}/wheelhouse
|
||||
|
||||
[testenv:lint]
|
||||
skipsdist = True
|
||||
skip_install = True
|
||||
@@ -55,7 +43,7 @@ deps =
|
||||
pygments
|
||||
restructuredtext_lint
|
||||
commands =
|
||||
black --check --diff setup.py install_python_bindings.py src/deltachat examples/ tests/
|
||||
black --quiet --check --diff setup.py src/deltachat examples/ tests/
|
||||
ruff src/deltachat tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
|
||||
1
release-date.in
Normal file
1
release-date.in
Normal file
@@ -0,0 +1 @@
|
||||
2023-04-24
|
||||
@@ -8,6 +8,8 @@ and an own build machine.
|
||||
|
||||
- `clippy.sh` runs `cargo clippy` for all Rust code in the project.
|
||||
|
||||
- `deny.sh` runs `cargo deny` for all Rust code in the project.
|
||||
|
||||
- `../.github/workflows` contains jobs run by GitHub Actions.
|
||||
|
||||
- `remote_tests_python.sh` rsyncs to a build machine and runs
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Build deltachat-rpc-server for Android.
|
||||
|
||||
set -e
|
||||
|
||||
test -n "$ANDROID_NDK_ROOT" || exit 1
|
||||
|
||||
RUSTUP_TOOLCHAIN="1.64.0"
|
||||
rustup install "$RUSTUP_TOOLCHAIN"
|
||||
rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android --toolchain "$RUSTUP_TOOLCHAIN"
|
||||
|
||||
KERNEL="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
ARCH="$(uname -m)"
|
||||
NDK_HOST_TAG="$KERNEL-$ARCH"
|
||||
TOOLCHAIN="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$NDK_HOST_TAG"
|
||||
|
||||
PACKAGE="deltachat-rpc-server"
|
||||
|
||||
export CARGO_PROFILE_RELEASE_LTO=on
|
||||
|
||||
CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="$TOOLCHAIN/bin/armv7a-linux-androideabi16-clang" \
|
||||
TARGET_CC="$TOOLCHAIN/bin/armv7a-linux-androideabi16-clang" \
|
||||
TARGET_AR="$TOOLCHAIN/bin/llvm-ar" \
|
||||
TARGET_RANLIB="$TOOLCHAIN/bin/llvm-ranlib" \
|
||||
cargo "+$RUSTUP_TOOLCHAIN" rustc --release --target armv7-linux-androideabi -p $PACKAGE
|
||||
|
||||
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$TOOLCHAIN/bin/aarch64-linux-android21-clang" \
|
||||
TARGET_CC="$TOOLCHAIN/bin/aarch64-linux-android21-clang" \
|
||||
TARGET_AR="$TOOLCHAIN/bin/llvm-ar" \
|
||||
TARGET_RANLIB="$TOOLCHAIN/bin/llvm-ranlib" \
|
||||
cargo "+$RUSTUP_TOOLCHAIN" rustc --release --target aarch64-linux-android -p $PACKAGE
|
||||
|
||||
CARGO_TARGET_I686_LINUX_ANDROID_LINKER="$TOOLCHAIN/bin/i686-linux-android16-clang" \
|
||||
TARGET_CC="$TOOLCHAIN/bin/i686-linux-android16-clang" \
|
||||
TARGET_AR="$TOOLCHAIN/bin/llvm-ar" \
|
||||
TARGET_RANLIB="$TOOLCHAIN/bin/llvm-ranlib" \
|
||||
cargo "+$RUSTUP_TOOLCHAIN" rustc --release --target i686-linux-android -p $PACKAGE
|
||||
|
||||
CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="$TOOLCHAIN/bin/x86_64-linux-android21-clang" \
|
||||
TARGET_CC="$TOOLCHAIN/bin/x86_64-linux-android21-clang" \
|
||||
TARGET_AR="$TOOLCHAIN/bin/llvm-ar" \
|
||||
TARGET_RANLIB="$TOOLCHAIN/bin/llvm-ranlib" \
|
||||
cargo "+$RUSTUP_TOOLCHAIN" rustc --release --target x86_64-linux-android -p $PACKAGE
|
||||
@@ -233,7 +233,7 @@ if __name__ == "__main__":
|
||||
else:
|
||||
now = datetime.datetime.fromisoformat(sys.argv[2])
|
||||
out_all += (
|
||||
"pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "
|
||||
"pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "
|
||||
"Lazy::new(|| chrono::NaiveDate::from_ymd_opt("
|
||||
+ str(now.year)
|
||||
+ ", "
|
||||
|
||||
6
scripts/deny.sh
Executable file
6
scripts/deny.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Update package cache without changing the lockfile.
|
||||
cargo update --dry-run
|
||||
|
||||
cargo deny --workspace --all-features check -D warnings
|
||||
39
scripts/release.py
Normal file
39
scripts/release.py
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
import set_core_version
|
||||
from argparse import ArgumentParser
|
||||
|
||||
|
||||
# update the version
|
||||
parser = ArgumentParser(prog="release")
|
||||
parser.add_argument("newversion")
|
||||
|
||||
try:
|
||||
newversion = parser.parse_args().newversion
|
||||
except SystemExit:
|
||||
newversion = None
|
||||
set_core_version.set_version(newversion)
|
||||
|
||||
tag = "v" + newversion
|
||||
|
||||
# TODO would be nice to automatically checkout the correct branch
|
||||
|
||||
# update the changelog
|
||||
print(f"Updating CHANGELOG.md using git cliff --unreleased --tag {newversion} --prepend CHANGELOG.md")
|
||||
changelog = subprocess.run(["git", "cliff", "--unreleased", "--tag", newversion, "--prepend", "CHANGELOG.md"]).stdout.strip()
|
||||
subprocess.run(["git", "add", "-A"])
|
||||
subprocess.run(["git", "commit", "-m", f"chore(release): prepare for {tag}"])
|
||||
subprocess.run(["git", "show"])
|
||||
|
||||
# create a tag
|
||||
subprocess.run(
|
||||
[
|
||||
"git", "tag", "-a", tag, "-m", f"Release {tag}", "-m", changelog
|
||||
]
|
||||
)
|
||||
subprocess.run(["git", "tag", "-v", tag])
|
||||
|
||||
print("Done!")
|
||||
print(f"Now push the commit (git push origin {tag}).")
|
||||
@@ -12,7 +12,7 @@ export DCC_RS_DEV=`pwd`
|
||||
|
||||
cd python
|
||||
|
||||
python install_python_bindings.py onlybuild
|
||||
cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
# remove and inhibit writing PYC files
|
||||
rm -rf tests/__pycache__
|
||||
@@ -22,4 +22,5 @@ export PYTHONDONTWRITEBYTECODE=1
|
||||
# run python tests (tox invokes pytest to run tests in python/tests)
|
||||
#TOX_PARALLEL_NO_SPINNER=1 tox -e lint,doc
|
||||
tox -e lint
|
||||
tox -e doc,py3
|
||||
tox -e doc
|
||||
tox -e py -- "$@"
|
||||
|
||||
@@ -31,7 +31,9 @@ unset DCC_NEW_TMP_EMAIL
|
||||
|
||||
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
|
||||
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39,auditwheels --skip-missing-interpreters true
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39 --skip-missing-interpreters true
|
||||
|
||||
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
|
||||
|
||||
|
||||
echo -----------------------
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
@@ -8,7 +9,14 @@ import subprocess
|
||||
from argparse import ArgumentParser
|
||||
|
||||
rex = re.compile(r'version = "(\S+)"')
|
||||
|
||||
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
|
||||
toml_list = [
|
||||
"Cargo.toml",
|
||||
"deltachat-ffi/Cargo.toml",
|
||||
"deltachat-jsonrpc/Cargo.toml",
|
||||
"deltachat-rpc-server/Cargo.toml",
|
||||
"deltachat-repl/Cargo.toml",
|
||||
]
|
||||
|
||||
def regex_matches(relpath, regex=rex):
|
||||
p = pathlib.Path(relpath)
|
||||
@@ -56,33 +64,19 @@ def update_package_json(relpath, newversion):
|
||||
json_data = json.loads(f.read())
|
||||
json_data["version"] = newversion
|
||||
with open(p, "w") as f:
|
||||
f.write(json.dumps(json_data, sort_keys=True, indent=2))
|
||||
json.dump(json_data, f, sort_keys=True, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(prog="set_core_version")
|
||||
parser.add_argument("newversion")
|
||||
|
||||
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
|
||||
toml_list = [
|
||||
"Cargo.toml",
|
||||
"deltachat-ffi/Cargo.toml",
|
||||
"deltachat-jsonrpc/Cargo.toml",
|
||||
"deltachat-rpc-server/Cargo.toml",
|
||||
"deltachat-repl/Cargo.toml",
|
||||
]
|
||||
try:
|
||||
opts = parser.parse_args()
|
||||
except SystemExit:
|
||||
def set_version(newversion):
|
||||
if newversion is None:
|
||||
print()
|
||||
for x in toml_list:
|
||||
print(f"{x}: {read_toml_version(x)}")
|
||||
for x in json_list:
|
||||
print(f"{x}: {read_json_version(x)}")
|
||||
print()
|
||||
raise SystemExit("need argument: new version, example: 1.25.0")
|
||||
raise SystemExit("need argument: newversion, example: 1.25.0")
|
||||
|
||||
newversion = opts.newversion
|
||||
if newversion.count(".") < 2:
|
||||
raise SystemExit("need at least two dots in version")
|
||||
|
||||
@@ -90,15 +84,25 @@ def main():
|
||||
ffi_toml = read_toml_version("deltachat-ffi/Cargo.toml")
|
||||
assert core_toml == ffi_toml, (core_toml, ffi_toml)
|
||||
|
||||
today = datetime.date.today().isoformat()
|
||||
|
||||
if "alpha" not in newversion:
|
||||
for line in open("CHANGELOG.md"):
|
||||
changelog_name = "CHANGELOG.md"
|
||||
changelog_tmpname = changelog_name + ".tmp"
|
||||
changelog_tmp = open(changelog_tmpname, "w")
|
||||
found = False
|
||||
for line in open(changelog_name):
|
||||
## 1.25.0
|
||||
if line.startswith("## ") and line[2:].strip().startswith(newversion):
|
||||
break
|
||||
else:
|
||||
if line == f"## [{newversion}]\n":
|
||||
line = f"## [{newversion}] - {today}\n"
|
||||
found = True
|
||||
changelog_tmp.write(line)
|
||||
if not found:
|
||||
raise SystemExit(
|
||||
f"CHANGELOG.md contains no entry for version: {newversion}"
|
||||
f"{changelog_name} contains no entry for version: {newversion}"
|
||||
)
|
||||
changelog_tmp.close()
|
||||
os.rename(changelog_tmpname, changelog_name)
|
||||
|
||||
for toml_filename in toml_list:
|
||||
replace_toml_version(toml_filename, newversion)
|
||||
@@ -106,6 +110,9 @@ def main():
|
||||
for json_filename in json_list:
|
||||
update_package_json(json_filename, newversion)
|
||||
|
||||
with open("release-date.in", "w") as f:
|
||||
f.write(today)
|
||||
|
||||
print("running cargo check")
|
||||
subprocess.call(["cargo", "check"])
|
||||
|
||||
@@ -119,6 +126,17 @@ def main():
|
||||
print(f" git push origin v{newversion}")
|
||||
print("")
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(prog="set_core_version")
|
||||
parser.add_argument("newversion")
|
||||
|
||||
try:
|
||||
newversion = parser.parse_args().newversion
|
||||
except SystemExit:
|
||||
newversion = None
|
||||
|
||||
set_version(newversion)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
32
scripts/zig-cc
Executable file
32
scripts/zig-cc
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def flag_filter(flag: str) -> bool:
|
||||
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 = []
|
||||
|
||||
subprocess.run(
|
||||
["zig", "cc", "-target", zig_target, *zig_cpu_args, *args], check=True
|
||||
)
|
||||
|
||||
|
||||
main()
|
||||
@@ -1,23 +1,52 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Build statically linked deltachat-rpc-server using cargo-zigbuild.
|
||||
# Build statically linked deltachat-rpc-server using zig.
|
||||
|
||||
set -x
|
||||
set -e
|
||||
|
||||
unset RUSTFLAGS
|
||||
|
||||
ZIG_VERSION=0.11.0-dev.1935+1d96a17af
|
||||
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
|
||||
export RUSTUP_TOOLCHAIN=1.68.1
|
||||
|
||||
ZIG_VERSION=0.11.0-dev.2213+515e1c93e
|
||||
|
||||
# Download Zig
|
||||
rm -fr "$ZIG_VERSION" "ZIG_VERSION.tar.xz"
|
||||
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
|
||||
wget "https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz"
|
||||
tar xf "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
|
||||
export PATH="$PWD/zig-linux-x86_64-$ZIG_VERSION:$PATH"
|
||||
|
||||
cargo install cargo-zigbuild
|
||||
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
|
||||
|
||||
for TARGET in aarch64-unknown-linux-musl armv7-unknown-linux-musleabihf; do
|
||||
rustup target add "$TARGET"
|
||||
cargo zigbuild --release --target "$TARGET" -p deltachat-rpc-server --features vendored
|
||||
done
|
||||
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
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::path::{Path, PathBuf};
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::Context;
|
||||
@@ -17,6 +18,7 @@ use crate::stock_str::StockStrings;
|
||||
pub struct Accounts {
|
||||
dir: PathBuf,
|
||||
config: Config,
|
||||
/// Map from account ID to the account.
|
||||
accounts: BTreeMap<u32, Context>,
|
||||
|
||||
/// Event channel to emit account manager errors.
|
||||
@@ -77,12 +79,12 @@ impl Accounts {
|
||||
})
|
||||
}
|
||||
|
||||
/// Get an account by its `id`:
|
||||
/// Returns an account by its `id`:
|
||||
pub fn get_account(&self, id: u32) -> Option<Context> {
|
||||
self.accounts.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Get the currently selected account.
|
||||
/// Returns the currently selected account.
|
||||
pub fn get_selected_account(&self) -> Option<Context> {
|
||||
let id = self.config.get_selected_account();
|
||||
self.accounts.get(&id).cloned()
|
||||
@@ -96,14 +98,14 @@ impl Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the given account.
|
||||
/// Selects the given account.
|
||||
pub async fn select_account(&mut self, id: u32) -> Result<()> {
|
||||
self.config.select_account(id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a new account and opens it.
|
||||
/// Adds a new account and opens it.
|
||||
///
|
||||
/// Returns account ID.
|
||||
pub async fn add_account(&mut self) -> Result<u32> {
|
||||
@@ -138,7 +140,7 @@ impl Accounts {
|
||||
Ok(account_config.id)
|
||||
}
|
||||
|
||||
/// Remove an account.
|
||||
/// Removes an account.
|
||||
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
|
||||
let ctx = self
|
||||
.accounts
|
||||
@@ -159,7 +161,7 @@ impl Accounts {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Migrate an existing account into this structure.
|
||||
/// Migrates an existing account into this structure.
|
||||
///
|
||||
/// Returns the ID of new account.
|
||||
pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
|
||||
@@ -262,7 +264,7 @@ impl Accounts {
|
||||
pub async fn stop_io(&self) {
|
||||
// Sending an event here wakes up event loop even
|
||||
// if there are no accounts.
|
||||
info!(self, "Stopping IO for all accounts");
|
||||
info!(self, "Stopping IO for all accounts.");
|
||||
for account in self.accounts.values() {
|
||||
account.stop_io().await;
|
||||
}
|
||||
@@ -271,14 +273,14 @@ impl Accounts {
|
||||
/// Notifies all accounts that the network may have become available.
|
||||
pub async fn maybe_network(&self) {
|
||||
for account in self.accounts.values() {
|
||||
account.maybe_network().await;
|
||||
account.scheduler.maybe_network().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifies all accounts that the network connection may have been lost.
|
||||
pub async fn maybe_network_lost(&self) {
|
||||
for account in self.accounts.values() {
|
||||
account.maybe_network_lost().await;
|
||||
account.scheduler.maybe_network_lost(account).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +303,7 @@ pub const DB_NAME: &str = "dc.db";
|
||||
|
||||
/// Account manager configuration file.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Config {
|
||||
struct Config {
|
||||
file: PathBuf,
|
||||
inner: InnerConfig,
|
||||
}
|
||||
@@ -325,10 +327,8 @@ impl Config {
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
};
|
||||
let cfg = Config {
|
||||
file: dir.join(CONFIG_NAME),
|
||||
inner,
|
||||
};
|
||||
let file = dir.join(CONFIG_NAME);
|
||||
let mut cfg = Self { file, inner };
|
||||
|
||||
cfg.sync().await?;
|
||||
|
||||
@@ -336,10 +336,24 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Sync the inmemory representation to disk.
|
||||
async fn sync(&self) -> Result<()> {
|
||||
fs::write(&self.file, toml::to_string_pretty(&self.inner)?)
|
||||
/// Takes a mutable reference because the saved file is a part of the `Config` state. This
|
||||
/// protects from parallel calls resulting to a wrong file contents.
|
||||
async fn sync(&mut self) -> Result<()> {
|
||||
let tmp_path = self.file.with_extension("toml.tmp");
|
||||
let mut file = fs::File::create(&tmp_path)
|
||||
.await
|
||||
.context("failed to write config")
|
||||
.context("failed to create a tmp config")?;
|
||||
file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
|
||||
.await
|
||||
.context("failed to write a tmp config")?;
|
||||
file.sync_data()
|
||||
.await
|
||||
.context("failed to sync a tmp config")?;
|
||||
drop(file);
|
||||
fs::rename(&tmp_path, &self.file)
|
||||
.await
|
||||
.context("failed to rename config")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read a configuration from the given file into memory.
|
||||
@@ -359,7 +373,7 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
let config = Self { file, inner };
|
||||
let mut config = Self { file, inner };
|
||||
if modified {
|
||||
config.sync().await?;
|
||||
}
|
||||
@@ -503,17 +517,19 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts1");
|
||||
|
||||
let mut accounts1 = Accounts::new(p.clone()).await.unwrap();
|
||||
accounts1.add_account().await.unwrap();
|
||||
{
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
accounts.add_account().await.unwrap();
|
||||
|
||||
let accounts2 = Accounts::open(p).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
}
|
||||
{
|
||||
let accounts = Accounts::open(p).await.unwrap();
|
||||
|
||||
assert_eq!(accounts1.accounts.len(), 1);
|
||||
assert_eq!(accounts1.config.get_selected_account(), 1);
|
||||
|
||||
assert_eq!(accounts1.dir, accounts2.dir);
|
||||
assert_eq!(accounts1.config, accounts2.config,);
|
||||
assert_eq!(accounts1.accounts.len(), accounts2.accounts.len());
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -308,7 +308,7 @@ async fn dkim_works_timestamp(context: &Context, from_domain: &str) -> Result<i6
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT dkim_works FROM sending_domains WHERE domain=?",
|
||||
paramsv![from_domain],
|
||||
(from_domain,),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
@@ -325,7 +325,7 @@ async fn set_dkim_works_timestamp(
|
||||
.execute(
|
||||
"INSERT INTO sending_domains (domain, dkim_works) VALUES (?,?)
|
||||
ON CONFLICT(domain) DO UPDATE SET dkim_works=excluded.dkim_works",
|
||||
paramsv![from_domain, timestamp],
|
||||
(from_domain, timestamp),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
498
src/blob.rs
498
src/blob.rs
@@ -4,23 +4,22 @@ use core::cmp::max;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::iter::FusedIterator;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use image::{DynamicImage, ImageFormat};
|
||||
use futures::StreamExt;
|
||||
use image::{DynamicImage, ImageFormat, ImageOutputFormat};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::{fs, io};
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
MediaQuality, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE, WORSE_IMAGE_SIZE,
|
||||
};
|
||||
use crate::constants::{self, MediaQuality};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
use crate::message;
|
||||
use crate::message::Viewtype;
|
||||
|
||||
/// Represents a file in the blob directory.
|
||||
///
|
||||
@@ -89,7 +88,7 @@ impl<'a> BlobObject<'a> {
|
||||
if attempt >= MAX_ATTEMPT {
|
||||
return Err(err).context("failed to create file");
|
||||
} else if attempt == 1 && !dir.exists() {
|
||||
fs::create_dir_all(dir).await.ok_or_log(context);
|
||||
fs::create_dir_all(dir).await.log_err(context).ok();
|
||||
} else {
|
||||
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
|
||||
}
|
||||
@@ -160,9 +159,9 @@ impl<'a> BlobObject<'a> {
|
||||
pub fn from_path(context: &'a Context, path: &Path) -> Result<BlobObject<'a>> {
|
||||
let rel_path = path
|
||||
.strip_prefix(context.get_blobdir())
|
||||
.context("wrong blobdir")?;
|
||||
.with_context(|| format!("wrong blobdir: {}", path.display()))?;
|
||||
if !BlobObject::is_acceptible_blob_name(rel_path) {
|
||||
return Err(format_err!("wrong name"));
|
||||
return Err(format_err!("bad blob name: {}", rel_path.display()));
|
||||
}
|
||||
let name = rel_path.to_str().context("wrong name")?;
|
||||
BlobObject::from_name(context, name.to_string())
|
||||
@@ -320,119 +319,149 @@ impl<'a> BlobObject<'a> {
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
MediaQuality::Balanced => BALANCED_AVATAR_SIZE,
|
||||
MediaQuality::Worse => WORSE_AVATAR_SIZE,
|
||||
MediaQuality::Balanced => constants::BALANCED_AVATAR_SIZE,
|
||||
MediaQuality::Worse => constants::WORSE_AVATAR_SIZE,
|
||||
};
|
||||
|
||||
let strict_limits = true;
|
||||
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
|
||||
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
|
||||
if let Some(new_name) = self.recode_to_size(context, blob_abs, img_wh, Some(20_000))? {
|
||||
if let Some(new_name) =
|
||||
self.recode_to_size(context, blob_abs, img_wh, 20_000, strict_limits)?
|
||||
{
|
||||
self.name = new_name;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn recode_to_image_size(&self, context: &Context) -> Result<()> {
|
||||
pub async fn recode_to_image_size(&mut self, context: &Context) -> Result<()> {
|
||||
let blob_abs = self.to_abs_path();
|
||||
if message::guess_msgtype_from_suffix(Path::new(&blob_abs))
|
||||
!= Some((Viewtype::Image, "image/jpeg"))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let img_wh =
|
||||
let (img_wh, max_bytes) =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
MediaQuality::Balanced => BALANCED_IMAGE_SIZE,
|
||||
MediaQuality::Worse => WORSE_IMAGE_SIZE,
|
||||
MediaQuality::Balanced => (
|
||||
constants::BALANCED_IMAGE_SIZE,
|
||||
constants::BALANCED_IMAGE_BYTES,
|
||||
),
|
||||
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
|
||||
};
|
||||
|
||||
if self
|
||||
.recode_to_size(context, blob_abs, img_wh, None)?
|
||||
.is_some()
|
||||
let strict_limits = false;
|
||||
if let Some(new_name) =
|
||||
self.recode_to_size(context, blob_abs, img_wh, max_bytes, strict_limits)?
|
||||
{
|
||||
return Err(format_err!(
|
||||
"Internal error: recode_to_size(..., None) shouldn't change the name of the image"
|
||||
));
|
||||
self.name = new_name;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just
|
||||
/// proceed with the result.
|
||||
fn recode_to_size(
|
||||
&self,
|
||||
context: &Context,
|
||||
mut blob_abs: PathBuf,
|
||||
mut img_wh: u32,
|
||||
max_bytes: Option<usize>,
|
||||
max_bytes: usize,
|
||||
strict_limits: bool,
|
||||
) -> Result<Option<String>> {
|
||||
tokio::task::block_in_place(move || {
|
||||
let mut img = image::open(&blob_abs).context("image recode failure")?;
|
||||
let orientation = self.get_exif_orientation(context);
|
||||
let mut img = image::open(&blob_abs).context("image decode failure")?;
|
||||
let (nr_bytes, exif) = self.metadata()?;
|
||||
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
|
||||
let mut encoded = Vec::new();
|
||||
let mut changed_name = None;
|
||||
|
||||
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
|
||||
img = match orientation {
|
||||
Some(90) => img.rotate90(),
|
||||
Some(180) => img.rotate180(),
|
||||
Some(270) => img.rotate270(),
|
||||
_ => img,
|
||||
};
|
||||
|
||||
let do_scale =
|
||||
exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
|
||||
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
|
||||
let exceeds_wh = img.width() > img_wh || img.height() > img_wh;
|
||||
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
|
||||
|
||||
if do_scale || do_rotate {
|
||||
if do_rotate {
|
||||
img = match orientation {
|
||||
Ok(90) => img.rotate90(),
|
||||
Ok(180) => img.rotate180(),
|
||||
Ok(270) => img.rotate270(),
|
||||
_ => img,
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
if do_scale {
|
||||
if !exceeds_width {
|
||||
// The image is already smaller than img_wh, but exceeds max_bytes
|
||||
// We can directly start with trying to scale down to 2/3 of its current width
|
||||
img_wh = max(img.width(), img.height()) * 2 / 3
|
||||
}
|
||||
|
||||
loop {
|
||||
let new_img = img.thumbnail(img_wh, img_wh);
|
||||
|
||||
if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
|
||||
if img_wh < 20 {
|
||||
return Err(format_err!(
|
||||
"Failed to scale image to below {}B",
|
||||
max_bytes.unwrap_or_default()
|
||||
));
|
||||
}
|
||||
|
||||
img_wh = img_wh * 2 / 3;
|
||||
} else {
|
||||
if encoded.is_empty() {
|
||||
encode_img(&new_img, &mut encoded)?;
|
||||
}
|
||||
|
||||
info!(
|
||||
};
|
||||
// We need to rewrite images with Exif to remove metadata such as location,
|
||||
// camera model, etc.
|
||||
//
|
||||
// TODO: Fix lost animation and transparency when recoding using the `image` crate. And
|
||||
// also `Viewtype::Gif` (maybe renamed to `Animation`) should be used for animated
|
||||
// images.
|
||||
let do_scale = exceeds_max_bytes
|
||||
|| strict_limits
|
||||
&& (exceeds_wh
|
||||
|| exif.is_some()
|
||||
&& encoded_img_exceeds_bytes(
|
||||
context,
|
||||
"Final scaled-down image size: {}B ({}px)",
|
||||
encoded.len(),
|
||||
img_wh
|
||||
);
|
||||
break;
|
||||
}
|
||||
&img,
|
||||
ofmt.clone(),
|
||||
max_bytes,
|
||||
&mut encoded,
|
||||
)?);
|
||||
|
||||
if do_scale {
|
||||
if !exceeds_wh {
|
||||
img_wh = max(img.width(), img.height());
|
||||
// PNGs and WebPs may be huge because of animation, which is lost by the `image`
|
||||
// crate when recoding, so don't scale them down.
|
||||
if matches!(fmt, Ok(ImageFormat::Jpeg)) || !encoded.is_empty() {
|
||||
img_wh = img_wh * 2 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
// The file format is JPEG now, we may have to change the file extension
|
||||
if !matches!(ImageFormat::from_path(&blob_abs), Ok(ImageFormat::Jpeg)) {
|
||||
loop {
|
||||
let new_img = img.thumbnail(img_wh, img_wh);
|
||||
|
||||
if encoded_img_exceeds_bytes(
|
||||
context,
|
||||
&new_img,
|
||||
ofmt.clone(),
|
||||
max_bytes,
|
||||
&mut encoded,
|
||||
)? && strict_limits
|
||||
{
|
||||
if img_wh < 20 {
|
||||
return Err(format_err!(
|
||||
"Failed to scale image to below {}B.",
|
||||
max_bytes,
|
||||
));
|
||||
}
|
||||
|
||||
img_wh = img_wh * 2 / 3;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Final scaled-down image size: {}B ({}px).",
|
||||
encoded.len(),
|
||||
img_wh
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(_))
|
||||
{
|
||||
blob_abs = blob_abs.with_extension("jpg");
|
||||
let file_name = blob_abs.file_name().context("No avatar file name (???)")?;
|
||||
let file_name = blob_abs.file_name().context("No image file name (???)")?;
|
||||
let file_name = file_name.to_str().context("Filename is no UTF-8 (???)")?;
|
||||
changed_name = Some(format!("$BLOBDIR/{file_name}"));
|
||||
}
|
||||
|
||||
if encoded.is_empty() {
|
||||
encode_img(&img, &mut encoded)?;
|
||||
encode_img(&img, ofmt, &mut encoded)?;
|
||||
}
|
||||
|
||||
std::fs::write(&blob_abs, &encoded)
|
||||
@@ -443,56 +472,146 @@ impl<'a> BlobObject<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_exif_orientation(&self, context: &Context) -> Result<i32> {
|
||||
/// Returns image file size and Exif.
|
||||
pub fn metadata(&self) -> Result<(u64, Option<exif::Exif>)> {
|
||||
let file = std::fs::File::open(self.to_abs_path())?;
|
||||
let len = file.metadata()?.len();
|
||||
let mut bufreader = std::io::BufReader::new(&file);
|
||||
let exifreader = exif::Reader::new();
|
||||
let exif = exifreader.read_from_container(&mut bufreader)?;
|
||||
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
|
||||
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
|
||||
// we only use rotation, in practise, flipping is not used.
|
||||
match orientation.value.get_uint(0) {
|
||||
Some(3) => return Ok(180),
|
||||
Some(6) => return Ok(90),
|
||||
Some(8) => return Ok(270),
|
||||
other => warn!(context, "exif orientation value ignored: {:?}", other),
|
||||
}
|
||||
}
|
||||
Ok(0)
|
||||
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
|
||||
Ok((len, exif))
|
||||
}
|
||||
}
|
||||
|
||||
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
|
||||
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
|
||||
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
|
||||
// we only use rotation, in practise, flipping is not used.
|
||||
match orientation.value.get_uint(0) {
|
||||
Some(3) => return 180,
|
||||
Some(6) => return 90,
|
||||
Some(8) => return 270,
|
||||
other => warn!(context, "Exif orientation value ignored: {other:?}."),
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for BlobObject<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "$BLOBDIR/{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_img(img: &DynamicImage, encoded: &mut Vec<u8>) -> anyhow::Result<()> {
|
||||
/// All files in the blobdir.
|
||||
///
|
||||
/// This exists so we can have a [`BlobDirIter`] which needs something to own the data of
|
||||
/// it's `&Path`. Use [`BlobDirContents::iter`] to create the iterator.
|
||||
///
|
||||
/// Additionally pre-allocating this means we get a length for progress report.
|
||||
pub(crate) struct BlobDirContents<'a> {
|
||||
inner: Vec<PathBuf>,
|
||||
context: &'a Context,
|
||||
}
|
||||
|
||||
impl<'a> BlobDirContents<'a> {
|
||||
pub(crate) async fn new(context: &'a Context) -> Result<BlobDirContents<'a>> {
|
||||
let readdir = fs::read_dir(context.get_blobdir()).await?;
|
||||
let inner = ReadDirStream::new(readdir)
|
||||
.filter_map(|entry| async move {
|
||||
match entry {
|
||||
Ok(entry) => Some(entry),
|
||||
Err(err) => {
|
||||
error!(context, "Failed to read blob file: {err}.");
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter_map(|entry| async move {
|
||||
match entry.file_type().await.ok()?.is_file() {
|
||||
true => Some(entry.path()),
|
||||
false => {
|
||||
warn!(
|
||||
context,
|
||||
"Export: Found blob dir entry {} that is not a file, ignoring.",
|
||||
entry.path().display()
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
Ok(Self { inner, context })
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> BlobDirIter<'_> {
|
||||
BlobDirIter::new(self.context, self.inner.iter())
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// A iterator over all the [`BlobObject`]s in the blobdir.
|
||||
pub(crate) struct BlobDirIter<'a> {
|
||||
iter: std::slice::Iter<'a, PathBuf>,
|
||||
context: &'a Context,
|
||||
}
|
||||
|
||||
impl<'a> BlobDirIter<'a> {
|
||||
fn new(context: &'a Context, iter: std::slice::Iter<'a, PathBuf>) -> BlobDirIter<'a> {
|
||||
Self { iter, context }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for BlobDirIter<'a> {
|
||||
type Item = BlobObject<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
for path in self.iter.by_ref() {
|
||||
// In theory this can error but we'd have corrupted filenames in the blobdir, so
|
||||
// silently skipping them is fine.
|
||||
match BlobObject::from_path(self.context, path) {
|
||||
Ok(blob) => return Some(blob),
|
||||
Err(err) => warn!(self.context, "{err}"),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for BlobDirIter<'_> {}
|
||||
|
||||
fn encode_img(
|
||||
img: &DynamicImage,
|
||||
fmt: ImageOutputFormat,
|
||||
encoded: &mut Vec<u8>,
|
||||
) -> anyhow::Result<()> {
|
||||
encoded.clear();
|
||||
let mut buf = Cursor::new(encoded);
|
||||
img.write_to(&mut buf, image::ImageFormat::Jpeg)?;
|
||||
img.write_to(&mut buf, fmt)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encoded_img_exceeds_bytes(
|
||||
context: &Context,
|
||||
img: &DynamicImage,
|
||||
max_bytes: Option<usize>,
|
||||
fmt: ImageOutputFormat,
|
||||
max_bytes: usize,
|
||||
encoded: &mut Vec<u8>,
|
||||
) -> anyhow::Result<bool> {
|
||||
if let Some(max_bytes) = max_bytes {
|
||||
encode_img(img, encoded)?;
|
||||
if encoded.len() > max_bytes {
|
||||
info!(
|
||||
context,
|
||||
"image size {}B ({}x{}px) exceeds {}B, need to scale down",
|
||||
encoded.len(),
|
||||
img.width(),
|
||||
img.height(),
|
||||
max_bytes,
|
||||
);
|
||||
return Ok(true);
|
||||
}
|
||||
encode_img(img, fmt, encoded)?;
|
||||
if encoded.len() > max_bytes {
|
||||
info!(
|
||||
context,
|
||||
"Image size {}B ({}x{}px) exceeds {}B, need to scale down.",
|
||||
encoded.len(),
|
||||
img.width(),
|
||||
img.height(),
|
||||
max_bytes,
|
||||
);
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
@@ -505,7 +624,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{self, create_group_chat, ProtectionStatus};
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::test_utils::{self, TestContext};
|
||||
|
||||
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
|
||||
@@ -730,7 +849,11 @@ mod tests {
|
||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||
|
||||
check_image_size(avatar_src, 1000, 1000);
|
||||
check_image_size(&avatar_blob, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE);
|
||||
check_image_size(
|
||||
&avatar_blob,
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
);
|
||||
|
||||
async fn file_size(path_buf: &Path) -> u64 {
|
||||
let file = File::open(path_buf).await.unwrap();
|
||||
@@ -738,8 +861,8 @@ mod tests {
|
||||
}
|
||||
|
||||
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
|
||||
|
||||
blob.recode_to_size(&t, blob.to_abs_path(), 1000, Some(3000))
|
||||
let strict_limits = true;
|
||||
blob.recode_to_size(&t, blob.to_abs_path(), 1000, 3000, strict_limits)
|
||||
.unwrap();
|
||||
assert!(file_size(&avatar_blob).await <= 3000);
|
||||
assert!(file_size(&avatar_blob).await > 2000);
|
||||
@@ -766,10 +889,14 @@ mod tests {
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
avatar_cfg,
|
||||
avatar_src.with_extension("jpg").to_str().unwrap()
|
||||
avatar_src.with_extension("png").to_str().unwrap()
|
||||
);
|
||||
|
||||
check_image_size(avatar_cfg, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE);
|
||||
check_image_size(
|
||||
avatar_cfg,
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -795,18 +922,29 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_1() {
|
||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
|
||||
send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000)
|
||||
.await
|
||||
.unwrap();
|
||||
send_image_check_mediaquality(
|
||||
Some("1"),
|
||||
Some("0"),
|
||||
bytes,
|
||||
"jpg",
|
||||
true, // has Exif
|
||||
1000,
|
||||
1000,
|
||||
0,
|
||||
WORSE_IMAGE_SIZE,
|
||||
WORSE_IMAGE_SIZE,
|
||||
1000,
|
||||
1000,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_image_check_mediaquality(
|
||||
Some("1"),
|
||||
bytes,
|
||||
"jpg",
|
||||
true, // has Exif
|
||||
1000,
|
||||
1000,
|
||||
0,
|
||||
1000,
|
||||
1000,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -819,69 +957,87 @@ mod tests {
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Some("0"),
|
||||
bytes,
|
||||
"jpg",
|
||||
true, // has Exif
|
||||
2000,
|
||||
1800,
|
||||
270,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
1800,
|
||||
2000,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let mut buf = Cursor::new(vec![]);
|
||||
img_rotated
|
||||
.write_to(&mut buf, image::ImageFormat::Jpeg)
|
||||
.unwrap();
|
||||
img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
|
||||
let bytes = buf.into_inner();
|
||||
|
||||
// Do this in parallel to speed up the test a bit
|
||||
// (it still takes very long though)
|
||||
let bytes2 = bytes.clone();
|
||||
let join_handle = tokio::task::spawn(async move {
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Some("0"),
|
||||
&bytes2,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
0,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
});
|
||||
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Some("1"),
|
||||
&bytes,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
"jpg",
|
||||
false, // no Exif
|
||||
1800,
|
||||
2000,
|
||||
0,
|
||||
WORSE_IMAGE_SIZE * 1800 / 2000,
|
||||
WORSE_IMAGE_SIZE,
|
||||
1800,
|
||||
2000,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
join_handle.await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_3() {
|
||||
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
|
||||
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
async fn test_recode_image_balanced_png() {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot.png");
|
||||
|
||||
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
|
||||
let img_rotated = send_image_check_mediaquality(Some("1"), bytes, 200, 180, 270, 180, 200)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
send_image_check_mediaquality(
|
||||
Some("0"),
|
||||
bytes,
|
||||
"png",
|
||||
false, // no Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
1920,
|
||||
1080,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_image_check_mediaquality(
|
||||
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");
|
||||
send_image_check_mediaquality(
|
||||
Some("0"),
|
||||
bytes,
|
||||
"jpg",
|
||||
true, // has Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
constants::BALANCED_IMAGE_SIZE,
|
||||
constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn assert_correct_rotation(img: &DynamicImage) {
|
||||
@@ -901,9 +1057,12 @@ mod tests {
|
||||
assert_eq!(luma, 0);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn send_image_check_mediaquality(
|
||||
media_quality_config: Option<&str>,
|
||||
bytes: &[u8],
|
||||
extension: &str,
|
||||
has_exif: bool,
|
||||
original_width: u32,
|
||||
original_height: u32,
|
||||
orientation: i32,
|
||||
@@ -915,7 +1074,7 @@ mod tests {
|
||||
alice
|
||||
.set_config(Config::MediaQuality, media_quality_config)
|
||||
.await?;
|
||||
let file = alice.get_blobdir().join("file.jpg");
|
||||
let file = alice.get_blobdir().join("file").with_extension(extension);
|
||||
|
||||
fs::write(&file, &bytes)
|
||||
.await
|
||||
@@ -923,7 +1082,13 @@ mod tests {
|
||||
check_image_size(&file, original_width, original_height);
|
||||
|
||||
let blob = BlobObject::new_from_path(&alice, &file).await?;
|
||||
assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation);
|
||||
let (_, exif) = blob.metadata()?;
|
||||
if has_exif {
|
||||
let exif = exif.unwrap();
|
||||
assert_eq!(exif_orientation(&exif, &alice), orientation);
|
||||
} else {
|
||||
assert!(exif.is_none());
|
||||
}
|
||||
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
@@ -944,7 +1109,8 @@ mod tests {
|
||||
let file = bob_msg.get_file(&bob).unwrap();
|
||||
|
||||
let blob = BlobObject::new_from_path(&bob, &file).await?;
|
||||
assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0);
|
||||
let (_, exif) = blob.metadata()?;
|
||||
assert!(exif.is_none());
|
||||
|
||||
let img = check_image_size(file, compressed_width, compressed_height);
|
||||
Ok(img)
|
||||
|
||||
321
src/chat.rs
321
src/chat.rs
@@ -35,8 +35,9 @@ use crate::scheduler::InterruptInfo;
|
||||
use crate::smtp::send_msg_to_smtp;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{
|
||||
create_id, create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps,
|
||||
get_abs_path, gm2local_offset, improve_single_line_input, time, IsNoneOrEmpty,
|
||||
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
|
||||
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
|
||||
strip_rtlo_characters, time, IsNoneOrEmpty,
|
||||
};
|
||||
use crate::webxdc::WEBXDC_SUFFIX;
|
||||
use crate::{location, sql};
|
||||
@@ -247,7 +248,7 @@ impl ChatId {
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot create chat, contact {} does not exist.", contact_id,
|
||||
"Cannot create chat, contact {contact_id} does not exist."
|
||||
);
|
||||
bail!("Can not create chat for non-existing contact");
|
||||
}
|
||||
@@ -268,25 +269,26 @@ impl ChatId {
|
||||
create_protected: ProtectionStatus,
|
||||
param: Option<String>,
|
||||
) -> Result<Self> {
|
||||
let grpname = strip_rtlo_characters(grpname);
|
||||
let row_id =
|
||||
context.sql.insert(
|
||||
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);",
|
||||
paramsv![
|
||||
(
|
||||
chattype,
|
||||
grpname,
|
||||
&grpname,
|
||||
grpid,
|
||||
create_blocked,
|
||||
create_smeared_timestamp(context),
|
||||
create_protected,
|
||||
param.unwrap_or_default(),
|
||||
],
|
||||
),
|
||||
).await?;
|
||||
|
||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||
info!(
|
||||
context,
|
||||
"Created group/mailinglist '{}' grpid={} as {}, blocked={}",
|
||||
grpname,
|
||||
"Created group/mailinglist '{}' grpid={} as {}, blocked={}.",
|
||||
&grpname,
|
||||
grpid,
|
||||
chat_id,
|
||||
create_blocked,
|
||||
@@ -302,7 +304,7 @@ impl ChatId {
|
||||
"UPDATE contacts
|
||||
SET selfavatar_sent=?
|
||||
WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=?);",
|
||||
paramsv![timestamp, self],
|
||||
(timestamp, self),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -319,7 +321,7 @@ impl ChatId {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET blocked=?1 WHERE id=?2 AND blocked != ?1",
|
||||
paramsv![new_blocked, self],
|
||||
(new_blocked, self),
|
||||
)
|
||||
.await?;
|
||||
Ok(count > 0)
|
||||
@@ -338,14 +340,14 @@ impl ChatId {
|
||||
if contact_id != ContactId::SELF {
|
||||
info!(
|
||||
context,
|
||||
"Blocking the contact {} to block 1:1 chat", contact_id
|
||||
"Blocking the contact {contact_id} to block 1:1 chat."
|
||||
);
|
||||
Contact::block(context, contact_id).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Chattype::Group => {
|
||||
info!(context, "Can't block groups yet, deleting the chat");
|
||||
info!(context, "Can't block groups yet, deleting the chat.");
|
||||
self.delete(context).await?;
|
||||
}
|
||||
Chattype::Mailinglist => {
|
||||
@@ -433,10 +435,7 @@ impl ChatId {
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET protected=? WHERE id=?;",
|
||||
paramsv![protect, self],
|
||||
)
|
||||
.execute("UPDATE chats SET protected=? WHERE id=?;", (protect, self))
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
@@ -500,7 +499,7 @@ impl ChatId {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
if let Err(e) = self.inner_set_protection(context, protect).await {
|
||||
error!(context, "Cannot set protection: {}", e); // make error user-visible
|
||||
error!(context, "Cannot set protection: {e:#}."); // make error user-visible
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
@@ -522,12 +521,12 @@ impl ChatId {
|
||||
if visibility == ChatVisibility::Archived {
|
||||
transaction.execute(
|
||||
"UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
|
||||
paramsv![MessageState::InNoticed, self, MessageState::InFresh],
|
||||
(MessageState::InNoticed, self, MessageState::InFresh),
|
||||
)?;
|
||||
}
|
||||
transaction.execute(
|
||||
"UPDATE chats SET archived=? WHERE id=?;",
|
||||
paramsv![visibility, self],
|
||||
(visibility, self),
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -556,7 +555,7 @@ impl ChatId {
|
||||
.execute(
|
||||
"UPDATE chats SET archived=0 WHERE id=? AND archived=1 \
|
||||
AND NOT(muted_until=-1 OR muted_until>?)",
|
||||
paramsv![self, time()],
|
||||
(self, time()),
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
@@ -574,7 +573,7 @@ impl ChatId {
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?",
|
||||
paramsv![MessageState::InFresh, self],
|
||||
(MessageState::InFresh, self),
|
||||
)
|
||||
.await?;
|
||||
if unread_cnt == 1 {
|
||||
@@ -585,7 +584,7 @@ impl ChatId {
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE chats SET archived=0 WHERE id=?", paramsv![self])
|
||||
.execute("UPDATE chats SET archived=0 WHERE id=?", (self,))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -614,32 +613,32 @@ impl ChatId {
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);",
|
||||
paramsv![self],
|
||||
(self,),
|
||||
)
|
||||
.await?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM msgs WHERE chat_id=?;", paramsv![self])
|
||||
.execute("DELETE FROM msgs WHERE chat_id=?;", (self,))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM chats_contacts WHERE chat_id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (self,))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM chats WHERE id=?;", paramsv![self])
|
||||
.execute("DELETE FROM chats WHERE id=?;", (self,))
|
||||
.await?;
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
|
||||
context.set_config(Config::LastHousekeeping, None).await?;
|
||||
context.interrupt_inbox(InterruptInfo::new(false)).await;
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_inbox(InterruptInfo::new(false))
|
||||
.await;
|
||||
|
||||
if chat.is_self_talk() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
@@ -686,7 +685,7 @@ impl ChatId {
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT id FROM msgs WHERE chat_id=? AND state=?;",
|
||||
paramsv![self, MessageState::OutDraft],
|
||||
(self, MessageState::OutDraft),
|
||||
)
|
||||
.await?;
|
||||
Ok(msg_id)
|
||||
@@ -768,14 +767,14 @@ impl ChatId {
|
||||
"UPDATE msgs
|
||||
SET timestamp=?,type=?,txt=?, param=?,mime_in_reply_to=?
|
||||
WHERE id=?;",
|
||||
paramsv![
|
||||
(
|
||||
time(),
|
||||
msg.viewtype,
|
||||
msg.text.as_deref().unwrap_or(""),
|
||||
msg.param.to_string(),
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
msg.id
|
||||
],
|
||||
msg.id,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
return Ok(true);
|
||||
@@ -799,7 +798,7 @@ impl ChatId {
|
||||
hidden,
|
||||
mime_in_reply_to)
|
||||
VALUES (?,?,?, ?,?,?,?,?,?);",
|
||||
paramsv![
|
||||
(
|
||||
self,
|
||||
ContactId::SELF,
|
||||
time(),
|
||||
@@ -809,7 +808,7 @@ impl ChatId {
|
||||
msg.param.to_string(),
|
||||
1,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
msg.id = MsgId::new(row_id.try_into()?);
|
||||
@@ -822,7 +821,7 @@ impl ChatId {
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id=?",
|
||||
paramsv![self],
|
||||
(self,),
|
||||
)
|
||||
.await?;
|
||||
Ok(count)
|
||||
@@ -865,7 +864,7 @@ impl ChatId {
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?;",
|
||||
paramsv![MessageState::InFresh, self],
|
||||
(MessageState::InFresh, self),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
@@ -875,7 +874,7 @@ impl ChatId {
|
||||
pub(crate) async fn get_param(self, context: &Context) -> Result<Params> {
|
||||
let res: Option<String> = context
|
||||
.sql
|
||||
.query_get_value("SELECT param FROM chats WHERE id=?", paramsv![self])
|
||||
.query_get_value("SELECT param FROM chats WHERE id=?", (self,))
|
||||
.await?;
|
||||
Ok(res
|
||||
.map(|s| s.parse().unwrap_or_default())
|
||||
@@ -920,13 +919,13 @@ impl ChatId {
|
||||
let row = sql
|
||||
.query_row_optional(
|
||||
&query,
|
||||
paramsv![
|
||||
(
|
||||
self,
|
||||
MessageState::OutPreparing,
|
||||
MessageState::OutDraft,
|
||||
MessageState::OutPending,
|
||||
MessageState::OutFailed
|
||||
],
|
||||
MessageState::OutFailed,
|
||||
),
|
||||
f,
|
||||
)
|
||||
.await?;
|
||||
@@ -1048,10 +1047,7 @@ impl ChatId {
|
||||
pub async fn get_gossiped_timestamp(self, context: &Context) -> Result<i64> {
|
||||
let timestamp: Option<i64> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT gossiped_timestamp FROM chats WHERE id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
.query_get_value("SELECT gossiped_timestamp FROM chats WHERE id=?;", (self,))
|
||||
.await?;
|
||||
Ok(timestamp.unwrap_or_default())
|
||||
}
|
||||
@@ -1067,14 +1063,14 @@ impl ChatId {
|
||||
);
|
||||
info!(
|
||||
context,
|
||||
"set gossiped_timestamp for chat {} to {}.", self, timestamp,
|
||||
"Set gossiped_timestamp for chat {} to {}.", self, timestamp,
|
||||
);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
|
||||
paramsv![timestamp, self],
|
||||
(timestamp, self),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1170,7 +1166,7 @@ impl Chat {
|
||||
c.blocked, c.locations_send_until, c.muted_until, c.protected
|
||||
FROM chats c
|
||||
WHERE c.id=?;",
|
||||
paramsv![chat_id],
|
||||
(chat_id,),
|
||||
|row| {
|
||||
let c = Chat {
|
||||
id: chat_id,
|
||||
@@ -1208,7 +1204,7 @@ impl Chat {
|
||||
Err(err) => {
|
||||
error!(
|
||||
context,
|
||||
"failed to load contacts for {}: {:#}", chat.id, err
|
||||
"Failed to load contacts for {}: {:#}.", chat.id, err
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1284,7 +1280,7 @@ impl Chat {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET param=? WHERE id=?",
|
||||
paramsv![self.param.to_string(), self.id],
|
||||
(self.param.to_string(), self.id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1466,7 +1462,7 @@ impl Chat {
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT contact_id FROM chats_contacts WHERE chat_id=?;",
|
||||
paramsv![self.id],
|
||||
(self.id,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -1547,13 +1543,13 @@ impl Chat {
|
||||
"INSERT INTO locations \
|
||||
(timestamp,from_id,chat_id, latitude,longitude,independent)\
|
||||
VALUES (?,?,?, ?,?,1);",
|
||||
paramsv![
|
||||
(
|
||||
timestamp,
|
||||
ContactId::SELF,
|
||||
self.id,
|
||||
msg.param.get_float(Param::SetLatitude).unwrap_or_default(),
|
||||
msg.param.get_float(Param::SetLongitude).unwrap_or_default(),
|
||||
],
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1577,7 +1573,12 @@ impl Chat {
|
||||
} else {
|
||||
msg.param.get(Param::SendHtml).map(|s| s.to_string())
|
||||
};
|
||||
html.map(|html| new_html_mimepart(html).build().as_string())
|
||||
match html {
|
||||
Some(html) => Some(tokio::task::block_in_place(move || {
|
||||
buf_compress(new_html_mimepart(html).build().as_string().as_bytes())
|
||||
})?),
|
||||
None => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1591,9 +1592,10 @@ impl Chat {
|
||||
SET rfc724_mid=?, chat_id=?, from_id=?, to_id=?, timestamp=?, type=?,
|
||||
state=?, txt=?, subject=?, param=?,
|
||||
hidden=?, mime_in_reply_to=?, mime_references=?, mime_modified=?,
|
||||
mime_headers=?, location_id=?, ephemeral_timer=?, ephemeral_timestamp=?
|
||||
mime_headers=?, mime_compressed=1, location_id=?, ephemeral_timer=?,
|
||||
ephemeral_timestamp=?
|
||||
WHERE id=?;",
|
||||
paramsv![
|
||||
params_slice![
|
||||
new_rfc724_mid,
|
||||
self.id,
|
||||
ContactId::SELF,
|
||||
@@ -1637,11 +1639,12 @@ impl Chat {
|
||||
mime_references,
|
||||
mime_modified,
|
||||
mime_headers,
|
||||
mime_compressed,
|
||||
location_id,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
|
||||
paramsv![
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
|
||||
params_slice![
|
||||
new_rfc724_mid,
|
||||
self.id,
|
||||
ContactId::SELF,
|
||||
@@ -1663,11 +1666,12 @@ impl Chat {
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
context.new_msgs_notify.notify_one();
|
||||
msg.id = MsgId::new(u32::try_from(raw_id)?);
|
||||
|
||||
maybe_set_logging_xdc(context, msg, self.id).await?;
|
||||
}
|
||||
context.interrupt_ephemeral_task().await;
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
Ok(msg.id)
|
||||
}
|
||||
}
|
||||
@@ -1852,7 +1856,7 @@ async fn update_special_chat_name(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET name=? WHERE id=? AND name!=?",
|
||||
paramsv![name, chat_id, name],
|
||||
(&name, chat_id, &name),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1915,7 +1919,7 @@ impl ChatIdBlocked {
|
||||
WHERE c.type=100 -- 100 = Chattype::Single
|
||||
AND c.id>9 -- 9 = DC_CHAT_ID_LAST_SPECIAL
|
||||
AND j.contact_id=?;",
|
||||
paramsv![contact_id],
|
||||
(contact_id,),
|
||||
|row| {
|
||||
let id: ChatId = row.get(0)?;
|
||||
let blocked: Blocked = row.get(1)?;
|
||||
@@ -1966,13 +1970,13 @@ impl ChatIdBlocked {
|
||||
"INSERT INTO chats
|
||||
(type, name, param, blocked, created_timestamp)
|
||||
VALUES(?, ?, ?, ?, ?)",
|
||||
params![
|
||||
(
|
||||
Chattype::Single,
|
||||
chat_name,
|
||||
params.to_string(),
|
||||
create_blocked as u8,
|
||||
create_smeared_timestamp(context)
|
||||
],
|
||||
create_smeared_timestamp(context),
|
||||
),
|
||||
)?;
|
||||
let chat_id = ChatId::new(
|
||||
transaction
|
||||
@@ -1985,7 +1989,7 @@ impl ChatIdBlocked {
|
||||
"INSERT INTO chats_contacts
|
||||
(chat_id, contact_id)
|
||||
VALUES((SELECT last_insert_rowid()), ?)",
|
||||
params![contact_id],
|
||||
(contact_id,),
|
||||
)?;
|
||||
|
||||
Ok(chat_id)
|
||||
@@ -2022,15 +2026,18 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
|
||||
// the caller should check if the message text is empty
|
||||
} else if msg.viewtype.has_file() {
|
||||
let blob = msg
|
||||
let mut blob = msg
|
||||
.param
|
||||
.get_blob(Param::File, context, !msg.is_increation())
|
||||
.await?
|
||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||
|
||||
if msg.viewtype == Viewtype::Image {
|
||||
if let Err(e) = blob.recode_to_image_size(context).await {
|
||||
warn!(context, "Cannot recode image, using original data: {:?}", e);
|
||||
if let Err(err) = blob.recode_to_image_size(context).await {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot recode image, using original data: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
@@ -2142,7 +2149,7 @@ pub async fn is_contact_in_chat(
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=?;",
|
||||
paramsv![chat_id, contact_id],
|
||||
(chat_id, contact_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(exists)
|
||||
@@ -2194,6 +2201,13 @@ pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message
|
||||
}
|
||||
|
||||
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
// protect all system messages againts RTLO attacks
|
||||
if msg.is_system_message() {
|
||||
if let Some(text) = &msg.text {
|
||||
msg.text = Some(strip_rtlo_characters(text.as_ref()));
|
||||
}
|
||||
}
|
||||
|
||||
if prepare_send_msg(context, chat_id, msg).await?.is_some() {
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
|
||||
@@ -2201,7 +2215,10 @@ async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -
|
||||
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
|
||||
}
|
||||
|
||||
context.interrupt_smtp(InterruptInfo::new(false)).await;
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(msg.id)
|
||||
@@ -2250,7 +2267,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
|
||||
Ok(attach_selfavatar) => attach_selfavatar,
|
||||
Err(err) => {
|
||||
warn!(context, "job: cannot get selfavatar-state: {:#}", err);
|
||||
warn!(context, "SMTP job cannot get selfavatar-state: {err:#}.");
|
||||
false
|
||||
}
|
||||
};
|
||||
@@ -2277,7 +2294,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
// may happen eg. for groups with only SELF and bcc_self disabled
|
||||
info!(
|
||||
context,
|
||||
"message {} has no recipient, skipping smtp-send", msg_id
|
||||
"Message {msg_id} has no recipient, skipping smtp-send."
|
||||
);
|
||||
msg_id.set_delivered(context).await?;
|
||||
return Ok(None);
|
||||
@@ -2312,27 +2329,27 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
|
||||
if 0 != rendered_msg.last_added_location_id {
|
||||
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await {
|
||||
error!(context, "Failed to set kml sent_timestamp: {:#}", err);
|
||||
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
|
||||
}
|
||||
if !msg.hidden {
|
||||
if let Err(err) =
|
||||
location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id)
|
||||
.await
|
||||
{
|
||||
error!(context, "Failed to set msg_location_id: {:#}", err);
|
||||
error!(context, "Failed to set msg_location_id: {err:#}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
|
||||
if let Err(err) = context.delete_sync_ids(sync_ids).await {
|
||||
error!(context, "Failed to delete sync ids: {:#}", err);
|
||||
error!(context, "Failed to delete sync ids: {err:#}.");
|
||||
}
|
||||
}
|
||||
|
||||
if attach_selfavatar {
|
||||
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
|
||||
error!(context, "Failed to set selfavatar timestamp: {:#}", err);
|
||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2353,12 +2370,12 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
.insert(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
paramsv![
|
||||
(
|
||||
&rendered_msg.rfc724_mid,
|
||||
recipients,
|
||||
&rendered_msg.message,
|
||||
msg_id
|
||||
],
|
||||
msg_id,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(Some(row_id))
|
||||
@@ -2524,7 +2541,7 @@ pub async fn get_chat_msgs_ex(
|
||||
OR m.from_id == ?
|
||||
OR m.to_id == ?
|
||||
);",
|
||||
paramsv![chat_id, ContactId::INFO, ContactId::INFO],
|
||||
(chat_id, ContactId::INFO, ContactId::INFO),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
@@ -2537,7 +2554,7 @@ pub async fn get_chat_msgs_ex(
|
||||
FROM msgs m
|
||||
WHERE m.chat_id=?
|
||||
AND m.hidden=0;",
|
||||
paramsv![chat_id],
|
||||
(chat_id,),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
@@ -2555,7 +2572,7 @@ pub(crate) async fn marknoticed_chat_if_older_than(
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
|
||||
paramsv![chat_id],
|
||||
(chat_id,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -2605,7 +2622,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;",
|
||||
paramsv![MessageState::InFresh, chat_id],
|
||||
(MessageState::InFresh, chat_id),
|
||||
)
|
||||
.await?;
|
||||
if !exists {
|
||||
@@ -2620,7 +2637,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?;",
|
||||
paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id],
|
||||
(MessageState::InNoticed, MessageState::InFresh, chat_id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -2669,12 +2686,12 @@ pub(crate) async fn mark_old_messages_as_noticed(
|
||||
AND hidden=0
|
||||
AND chat_id=?
|
||||
AND timestamp<=?;",
|
||||
paramsv![
|
||||
(
|
||||
MessageState::InNoticed,
|
||||
MessageState::InFresh,
|
||||
msg.chat_id,
|
||||
msg.sort_timestamp
|
||||
],
|
||||
msg.sort_timestamp,
|
||||
),
|
||||
)?;
|
||||
if changed_rows > 0 {
|
||||
changed_chats.push(msg.chat_id);
|
||||
@@ -2687,8 +2704,7 @@ pub(crate) async fn mark_old_messages_as_noticed(
|
||||
if !changed_chats.is_empty() {
|
||||
info!(
|
||||
context,
|
||||
"Marking chats as noticed because there are newer outgoing messages: {:?}",
|
||||
changed_chats
|
||||
"Marking chats as noticed because there are newer outgoing messages: {changed_chats:?}."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2719,12 +2735,14 @@ pub async fn get_chat_media(
|
||||
"SELECT id
|
||||
FROM msgs
|
||||
WHERE (1=? OR chat_id=?)
|
||||
AND chat_id != ?
|
||||
AND (type=? OR type=? OR type=?)
|
||||
AND hidden=0
|
||||
ORDER BY timestamp, id;",
|
||||
paramsv![
|
||||
(
|
||||
chat_id.is_none(),
|
||||
chat_id.unwrap_or_else(|| ChatId::new(0)),
|
||||
DC_CHAT_ID_TRASH,
|
||||
msg_type,
|
||||
if msg_type2 != Viewtype::Unknown {
|
||||
msg_type2
|
||||
@@ -2736,7 +2754,7 @@ pub async fn get_chat_media(
|
||||
} else {
|
||||
msg_type
|
||||
},
|
||||
],
|
||||
),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|ids| Ok(ids.flatten().collect()),
|
||||
)
|
||||
@@ -2814,7 +2832,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec
|
||||
ON c.id=cc.contact_id
|
||||
WHERE cc.chat_id=?
|
||||
ORDER BY c.id=1, c.last_seen DESC, c.id DESC;",
|
||||
paramsv![chat_id],
|
||||
(chat_id,),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
@@ -2840,12 +2858,12 @@ pub async fn create_group_chat(
|
||||
"INSERT INTO chats
|
||||
(type, name, grpid, param, created_timestamp)
|
||||
VALUES(?, ?, ?, \'U=1\', ?);",
|
||||
paramsv![
|
||||
(
|
||||
Chattype::Group,
|
||||
chat_name,
|
||||
grpid,
|
||||
create_smeared_timestamp(context),
|
||||
],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -2878,7 +2896,7 @@ async fn find_unused_broadcast_list_name(context: &Context) -> Result<String> {
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM chats WHERE type=? AND name=?;",
|
||||
paramsv![Chattype::Broadcast, better_name],
|
||||
(Chattype::Broadcast, &better_name),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -2898,12 +2916,12 @@ pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
|
||||
"INSERT INTO chats
|
||||
(type, name, grpid, param, created_timestamp)
|
||||
VALUES(?, ?, ?, \'U=1\', ?);",
|
||||
paramsv![
|
||||
(
|
||||
Chattype::Broadcast,
|
||||
chat_name,
|
||||
grpid,
|
||||
create_smeared_timestamp(context),
|
||||
],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||
@@ -2924,7 +2942,7 @@ pub(crate) async fn add_to_chat_contacts_table(
|
||||
for contact_id in contact_ids {
|
||||
transaction.execute(
|
||||
"INSERT OR IGNORE INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
||||
paramsv![chat_id, contact_id],
|
||||
(chat_id, contact_id),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -2944,7 +2962,7 @@ pub(crate) async fn remove_from_chat_contacts_table(
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?",
|
||||
paramsv![chat_id, contact_id],
|
||||
(chat_id, contact_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -3009,7 +3027,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
// if SELF is not in the group, members cannot be added at all.
|
||||
warn!(
|
||||
context,
|
||||
"invalid attempt to add self e-mail address to group"
|
||||
"Invalid attempt to add self e-mail address to group."
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -3062,7 +3080,7 @@ pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId)
|
||||
FROM chats_contacts cc
|
||||
LEFT JOIN contacts c ON c.id=cc.contact_id
|
||||
WHERE cc.chat_id=? AND cc.contact_id!=?;",
|
||||
paramsv![chat_id, ContactId::SELF],
|
||||
(chat_id, ContactId::SELF),
|
||||
|row| Ok(row.get::<_, i64>(0)),
|
||||
|rows| {
|
||||
let mut needs_attach = false;
|
||||
@@ -3135,7 +3153,7 @@ pub async fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuratio
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
paramsv![duration, chat_id],
|
||||
(duration, chat_id),
|
||||
)
|
||||
.await
|
||||
.context(format!("Failed to set mute duration for {chat_id}"))?;
|
||||
@@ -3222,10 +3240,7 @@ async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()>
|
||||
if !is_group_explicitly_left(context, grpid).await? {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO leftgrps (grpid) VALUES(?);",
|
||||
paramsv![grpid.to_string()],
|
||||
)
|
||||
.execute("INSERT INTO leftgrps (grpid) VALUES(?);", (grpid,))
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -3235,10 +3250,7 @@ async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()>
|
||||
pub(crate) async fn is_group_explicitly_left(context: &Context, grpid: &str) -> Result<bool> {
|
||||
let exists = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM leftgrps WHERE grpid=?;",
|
||||
paramsv![grpid],
|
||||
)
|
||||
.exists("SELECT COUNT(*) FROM leftgrps WHERE grpid=?;", (grpid,))
|
||||
.await?;
|
||||
Ok(exists)
|
||||
}
|
||||
@@ -3270,7 +3282,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET name=? WHERE id=?;",
|
||||
paramsv![new_name.to_string(), chat_id],
|
||||
(new_name.to_string(), chat_id),
|
||||
)
|
||||
.await?;
|
||||
if chat.is_promoted()
|
||||
@@ -3433,7 +3445,10 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
.await?;
|
||||
curr_timestamp += 1;
|
||||
if create_send_msg_job(context, new_msg_id).await?.is_some() {
|
||||
context.interrupt_smtp(InterruptInfo::new(false)).await;
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
created_chats.push(chat_id);
|
||||
@@ -3488,7 +3503,10 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
msg_id: msg.id,
|
||||
});
|
||||
if create_send_msg_job(context, msg.id).await?.is_some() {
|
||||
context.interrupt_smtp(InterruptInfo::new(false)).await;
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3517,7 +3535,7 @@ pub(crate) async fn get_chat_id_by_grpid(
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id, blocked, protected FROM chats WHERE grpid=?;",
|
||||
paramsv![grpid],
|
||||
(grpid,),
|
||||
|row| {
|
||||
let chat_id = row.get::<_, ChatId>(0)?;
|
||||
|
||||
@@ -3550,7 +3568,7 @@ pub async fn add_device_msg_with_importance(
|
||||
|
||||
if let Some(label) = label {
|
||||
if was_device_msg_ever_added(context, label).await? {
|
||||
info!(context, "device-message {} already added", label);
|
||||
info!(context, "Device-message {label} already added.");
|
||||
return Ok(msg_id);
|
||||
}
|
||||
}
|
||||
@@ -3571,7 +3589,7 @@ pub async fn add_device_msg_with_importance(
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
|
||||
paramsv![chat_id],
|
||||
(chat_id,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -3596,7 +3614,7 @@ pub async fn add_device_msg_with_importance(
|
||||
param,
|
||||
rfc724_mid)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?);",
|
||||
paramsv![
|
||||
(
|
||||
chat_id,
|
||||
ContactId::DEVICE,
|
||||
ContactId::SELF,
|
||||
@@ -3608,9 +3626,10 @@ pub async fn add_device_msg_with_importance(
|
||||
msg.text.as_ref().cloned().unwrap_or_default(),
|
||||
msg.param.to_string(),
|
||||
rfc724_mid,
|
||||
],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
context.new_msgs_notify.notify_one();
|
||||
|
||||
msg_id = MsgId::new(u32::try_from(row_id)?);
|
||||
if !msg.hidden {
|
||||
@@ -3621,10 +3640,7 @@ pub async fn add_device_msg_with_importance(
|
||||
if let Some(label) = label {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO devmsglabels (label) VALUES (?);",
|
||||
paramsv![label.to_string()],
|
||||
)
|
||||
.execute("INSERT INTO devmsglabels (label) VALUES (?);", (label,))
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -3651,7 +3667,7 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(label) FROM devmsglabels WHERE label=?",
|
||||
paramsv![label],
|
||||
(label,),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -3668,10 +3684,7 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result
|
||||
pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM msgs WHERE from_id=?;",
|
||||
paramsv![ContactId::DEVICE],
|
||||
)
|
||||
.execute("DELETE FROM msgs WHERE from_id=?;", (ContactId::DEVICE,))
|
||||
.await?;
|
||||
context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
|
||||
|
||||
@@ -3714,7 +3727,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
context.sql.insert(
|
||||
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,timestamp_sent,timestamp_rcvd,type,state,txt,rfc724_mid,ephemeral_timer, param,mime_in_reply_to)
|
||||
VALUES (?,?,?, ?,?,?,?,?, ?,?,?, ?,?);",
|
||||
paramsv![
|
||||
(
|
||||
chat_id,
|
||||
from_id.unwrap_or(ContactId::INFO),
|
||||
ContactId::INFO,
|
||||
@@ -3728,8 +3741,9 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
ephemeral_timer,
|
||||
param.to_string(),
|
||||
parent.map(|msg|msg.rfc724_mid.clone()).unwrap_or_default()
|
||||
]
|
||||
)
|
||||
).await?;
|
||||
context.new_msgs_notify.notify_one();
|
||||
|
||||
let msg_id = MsgId::new(row_id.try_into()?);
|
||||
context.emit_msgs_changed(chat_id, msg_id);
|
||||
@@ -3768,7 +3782,7 @@ pub(crate) async fn update_msg_text_and_timestamp(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET txt=?, timestamp=? WHERE id=?;",
|
||||
paramsv![text, timestamp, msg_id],
|
||||
(text, timestamp, msg_id),
|
||||
)
|
||||
.await?;
|
||||
context.emit_msgs_changed(chat_id, msg_id);
|
||||
@@ -3781,8 +3795,10 @@ mod tests {
|
||||
use crate::chatlist::{get_archived_cnt, Chatlist};
|
||||
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::TestContext;
|
||||
use tokio::fs;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_chat_info() {
|
||||
@@ -5963,7 +5979,7 @@ mod tests {
|
||||
include_bytes!("../test-data/image/avatar64x64.png"),
|
||||
)
|
||||
.await?;
|
||||
send_media(
|
||||
let second_image_msg_id = send_media(
|
||||
&t,
|
||||
chat_id2,
|
||||
Viewtype::Image,
|
||||
@@ -6065,6 +6081,47 @@ mod tests {
|
||||
4
|
||||
);
|
||||
|
||||
// Delete an image.
|
||||
delete_msgs(&t, &[second_image_msg_id]).await?;
|
||||
assert_eq!(
|
||||
get_chat_media(
|
||||
&t,
|
||||
None,
|
||||
Viewtype::Image,
|
||||
Viewtype::Sticker,
|
||||
Viewtype::Webxdc,
|
||||
)
|
||||
.await?
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blob_renaming() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
chat_id,
|
||||
Contact::create(&alice, "bob", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
let dir = tempfile::tempdir()?;
|
||||
let file = dir.path().join("harmless_file.\u{202e}txt.exe");
|
||||
fs::write(&file, "aaa").await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let msg = bob.recv_msg(&alice.send_msg(chat_id, &mut msg).await).await;
|
||||
|
||||
// the file bob receives should not contain BIDI-control characters
|
||||
assert_eq!(
|
||||
Some("$BLOBDIR/harmless_file.txt.exe"),
|
||||
msg.param.get(Param::File),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ impl Chatlist {
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
|
||||
GROUP BY c.id
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned],
|
||||
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?
|
||||
@@ -164,7 +164,7 @@ impl Chatlist {
|
||||
AND c.archived=1
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft],
|
||||
(MessageState::OutDraft,),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
@@ -176,7 +176,7 @@ impl Chatlist {
|
||||
// allow searching over special names that may change at any time
|
||||
// when the ui calls set_stock_translation()
|
||||
if let Err(err) = update_special_chat_names(context).await {
|
||||
warn!(context, "cannot update special chat names: {:?}", err)
|
||||
warn!(context, "Cannot update special chat names: {err:#}.")
|
||||
}
|
||||
|
||||
let str_like_cmd = format!("%{query}%");
|
||||
@@ -198,7 +198,7 @@ impl Chatlist {
|
||||
AND c.name LIKE ?3
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, skip_id, str_like_cmd],
|
||||
(MessageState::OutDraft, skip_id, str_like_cmd),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
@@ -228,7 +228,7 @@ impl Chatlist {
|
||||
AND NOT c.archived=?4
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=?5 DESC, c.archived=?6 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
|
||||
(MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?;
|
||||
@@ -311,13 +311,17 @@ impl Chatlist {
|
||||
};
|
||||
|
||||
let (lastmsg, lastcontact) = if let Some(lastmsg_id) = lastmsg_id {
|
||||
let lastmsg = Message::load_from_db(context, lastmsg_id).await?;
|
||||
let lastmsg = Message::load_from_db(context, lastmsg_id)
|
||||
.await
|
||||
.context("loading message failed")?;
|
||||
if lastmsg.from_id == ContactId::SELF {
|
||||
(Some(lastmsg), None)
|
||||
} else {
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
|
||||
let lastcontact = Contact::load_from_db(context, lastmsg.from_id).await?;
|
||||
let lastcontact = Contact::load_from_db(context, lastmsg.from_id)
|
||||
.await
|
||||
.context("loading contact failed")?;
|
||||
(Some(lastmsg), Some(lastcontact))
|
||||
}
|
||||
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
|
||||
@@ -356,12 +360,31 @@ pub async fn get_archived_cnt(context: &Context) -> Result<usize> {
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM chats WHERE blocked!=? AND archived=?;",
|
||||
paramsv![Blocked::Yes, ChatVisibility::Archived],
|
||||
(Blocked::Yes, ChatVisibility::Archived),
|
||||
)
|
||||
.await?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Gets the last message of a chat, the message that would also be displayed in the ChatList
|
||||
/// Used for passing to `deltachat::chatlist::Chatlist::get_summary2`
|
||||
pub async fn get_last_message_for_chat(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
) -> Result<Option<MsgId>> {
|
||||
context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=?2
|
||||
AND (hidden=0 OR state=?1)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1",
|
||||
(MessageState::OutDraft, chat_id),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -308,6 +308,9 @@ pub enum Config {
|
||||
/// This value is used internally to remember the MsgId of the logging xdc
|
||||
#[strum(props(default = "0"))]
|
||||
DebugLogging,
|
||||
|
||||
/// Last message processed by the bot.
|
||||
LastMsgId,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -358,6 +361,11 @@ impl Context {
|
||||
Ok(self.get_config_parsed(key).await?.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Returns 32-bit unsigned integer configuration value for the given key.
|
||||
pub async fn get_config_u32(&self, key: Config) -> Result<u32> {
|
||||
Ok(self.get_config_parsed(key).await?.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Returns 64-bit signed integer configuration value for the given key.
|
||||
pub async fn get_config_i64(&self, key: Config) -> Result<i64> {
|
||||
Ok(self.get_config_parsed(key).await?.unwrap_or_default())
|
||||
@@ -443,7 +451,7 @@ impl Context {
|
||||
Config::DeleteDeviceAfter => {
|
||||
let ret = self.sql.set_raw_config(key.as_ref(), value).await;
|
||||
// Interrupt ephemeral loop to delete old messages immediately.
|
||||
self.interrupt_ephemeral_task().await;
|
||||
self.scheduler.interrupt_ephemeral_task().await;
|
||||
ret?
|
||||
}
|
||||
Config::Displayname => {
|
||||
@@ -459,6 +467,12 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the given config to an unsigned 32-bit integer value.
|
||||
pub async fn set_config_u32(&self, key: Config, value: u32) -> Result<()> {
|
||||
self.set_config(key, Some(&value.to_string())).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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") })
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
//! Email accounts autoconfiguration process module.
|
||||
//! # Email accounts autoconfiguration process.
|
||||
//!
|
||||
//! The module provides automatic lookup of configuration
|
||||
//! for email providers based on the built-in [provider database],
|
||||
//! [Mozilla Thunderbird Autoconfiguration protocol]
|
||||
//! and [Outlook's Autodiscover].
|
||||
//!
|
||||
//! [provider database]: crate::provider
|
||||
//! [Mozilla Thunderbird Autoconfiguration protocol]: auto_mozilla
|
||||
//! [Outlook's Autodiscover]: auto_outlook
|
||||
|
||||
mod auto_mozilla;
|
||||
mod auto_outlook;
|
||||
mod read_url;
|
||||
mod server_params;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
@@ -59,7 +67,7 @@ impl Context {
|
||||
/// Configures this account with the currently set parameters.
|
||||
pub async fn configure(&self) -> Result<()> {
|
||||
ensure!(
|
||||
self.scheduler.read().await.is_none(),
|
||||
!self.scheduler.is_running().await,
|
||||
"cannot configure, already running"
|
||||
);
|
||||
ensure!(
|
||||
@@ -156,7 +164,9 @@ async fn on_configure_completed(
|
||||
Some(stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await);
|
||||
chat::add_device_msg(context, None, Some(&mut msg))
|
||||
.await
|
||||
.ok_or_log_msg(context, "Cannot add AEAP explanation");
|
||||
.context("Cannot add AEAP explanation")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -469,7 +479,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
ctx.set_config_bool(Config::FetchedExistingMsgs, false)
|
||||
.await?;
|
||||
ctx.interrupt_inbox(InterruptInfo::new(false)).await;
|
||||
ctx.scheduler
|
||||
.interrupt_inbox(InterruptInfo::new(false))
|
||||
.await;
|
||||
|
||||
progress!(ctx, 940);
|
||||
update_device_chats_handle.await??;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
//! # Thunderbird's Autoconfiguration implementation
|
||||
//!
|
||||
//! Documentation: <https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration>
|
||||
//! Documentation: <https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration>
|
||||
use std::io::BufRead;
|
||||
use std::str::FromStr;
|
||||
|
||||
use quick_xml::events::{BytesStart, Event};
|
||||
|
||||
use super::read_url::read_url;
|
||||
use super::{Error, ServerParams};
|
||||
use crate::context::Context;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::net::read_url;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -7,9 +7,9 @@ use std::io::BufRead;
|
||||
|
||||
use quick_xml::events::Event;
|
||||
|
||||
use super::read_url::read_url;
|
||||
use super::{Error, ServerParams};
|
||||
use crate::context::Context;
|
||||
use crate::net::read_url;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
/// Result of parsing a single `Protocol` tag.
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
use anyhow::{anyhow, format_err};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::socks::Socks5Config;
|
||||
|
||||
pub async fn read_url(context: &Context, url: &str) -> anyhow::Result<String> {
|
||||
match read_url_inner(context, url).await {
|
||||
Ok(s) => {
|
||||
info!(context, "Successfully read url {}", url);
|
||||
Ok(s)
|
||||
}
|
||||
Err(e) => {
|
||||
info!(context, "Can't read URL {}: {:#}", url, e);
|
||||
Err(format_err!("Can't read URL {}: {:#}", url, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_url_inner(context: &Context, url: &str) -> anyhow::Result<String> {
|
||||
let socks5_config = Socks5Config::from_database(&context.sql).await?;
|
||||
let client = crate::http::get_client(socks5_config)?;
|
||||
let mut url = url.to_string();
|
||||
|
||||
// Follow up to 10 http-redirects
|
||||
for _i in 0..10 {
|
||||
let response = client.get(&url).send().await?;
|
||||
if response.status().is_redirection() {
|
||||
let headers = response.headers();
|
||||
let header = headers
|
||||
.get_all("location")
|
||||
.iter()
|
||||
.last()
|
||||
.ok_or_else(|| anyhow!("Redirection doesn't have a target location"))?
|
||||
.to_str()?;
|
||||
info!(context, "Following redirect to {}", header);
|
||||
url = header.to_string();
|
||||
continue;
|
||||
}
|
||||
|
||||
return response.text().await.map_err(Into::into);
|
||||
}
|
||||
|
||||
Err(format_err!("Followed 10 redirections"))
|
||||
}
|
||||
@@ -192,11 +192,15 @@ pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
|
||||
/// How many existing messages shall be fetched after configuration.
|
||||
pub(crate) const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
|
||||
|
||||
// max. weight of images to send w/o recoding
|
||||
pub const BALANCED_IMAGE_BYTES: usize = 500_000;
|
||||
pub const WORSE_IMAGE_BYTES: usize = 130_000;
|
||||
|
||||
// max. width/height of an avatar
|
||||
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 256;
|
||||
pub(crate) const WORSE_AVATAR_SIZE: u32 = 128;
|
||||
|
||||
// max. width/height of images
|
||||
// max. width/height of images scaled down because of being too huge
|
||||
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
|
||||
pub const WORSE_IMAGE_SIZE: u32 = 640;
|
||||
|
||||
|
||||
@@ -32,7 +32,10 @@ use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, EmailAddress};
|
||||
use crate::tools::{
|
||||
duration_to_str, get_abs_path, improve_single_line_input, strip_rtlo_characters, time,
|
||||
EmailAddress,
|
||||
};
|
||||
use crate::{chat, stock_str};
|
||||
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
@@ -344,7 +347,7 @@ impl Contact {
|
||||
c.authname, c.param, c.status
|
||||
FROM contacts c
|
||||
WHERE c.id=?;",
|
||||
paramsv![contact_id],
|
||||
(contact_id,),
|
||||
|row| {
|
||||
let name: String = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
@@ -457,7 +460,7 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
|
||||
paramsv![MessageState::InNoticed, id, MessageState::InFresh],
|
||||
(MessageState::InNoticed, id, MessageState::InFresh),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -490,7 +493,7 @@ impl Contact {
|
||||
"SELECT id FROM contacts \
|
||||
WHERE addr=?1 COLLATE NOCASE \
|
||||
AND id>?2 AND origin>=?3 AND blocked=0;",
|
||||
paramsv![addr_normalized, ContactId::LAST_SPECIAL, min_origin as u32,],
|
||||
(&addr_normalized, ContactId::LAST_SPECIAL, min_origin as u32),
|
||||
)
|
||||
.await?;
|
||||
Ok(id)
|
||||
@@ -536,7 +539,7 @@ impl Contact {
|
||||
return Ok((ContactId::SELF, sth_modified));
|
||||
}
|
||||
|
||||
let mut name = name;
|
||||
let mut name = strip_rtlo_characters(name);
|
||||
#[allow(clippy::collapsible_if)]
|
||||
if origin <= Origin::OutgoingTo {
|
||||
// The user may accidentally have written to a "noreply" address with another MUA:
|
||||
@@ -551,7 +554,7 @@ impl Contact {
|
||||
// For these kind of email addresses, sender and address often don't belong together
|
||||
// (like hocuri <notifications@github.com>). In this example, hocuri shouldn't
|
||||
// be saved as the displayname for notifications@github.com.
|
||||
name = "";
|
||||
name = "".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,7 +608,7 @@ impl Contact {
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
|
||||
paramsv![
|
||||
(
|
||||
new_name,
|
||||
if update_addr {
|
||||
addr.to_string()
|
||||
@@ -623,7 +626,7 @@ impl Contact {
|
||||
row_authname
|
||||
},
|
||||
row_id
|
||||
],
|
||||
),
|
||||
)?;
|
||||
|
||||
if update_name || update_authname {
|
||||
@@ -631,7 +634,7 @@ impl Contact {
|
||||
// This is one of the few duplicated data, however, getting the chat list is easier this way.
|
||||
let chat_id: Option<ChatId> = transaction.query_row(
|
||||
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
|
||||
params![Chattype::Single, isize::try_from(row_id)?],
|
||||
(Chattype::Single, isize::try_from(row_id)?),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
Ok(chat_id)
|
||||
@@ -645,7 +648,7 @@ impl Contact {
|
||||
"SELECT addr, name, authname
|
||||
FROM contacts
|
||||
WHERE id=?",
|
||||
params![contact_id],
|
||||
(contact_id,),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
let name: String = row.get(1)?;
|
||||
@@ -663,7 +666,7 @@ impl Contact {
|
||||
|
||||
let count = transaction.execute(
|
||||
"UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1",
|
||||
params![chat_name, chat_id])?;
|
||||
(chat_name, chat_id))?;
|
||||
|
||||
if count > 0 {
|
||||
// Chat name updated
|
||||
@@ -681,7 +684,7 @@ impl Contact {
|
||||
.execute(
|
||||
"INSERT INTO contacts (name, addr, origin, authname)
|
||||
VALUES (?, ?, ?, ?);",
|
||||
params![
|
||||
(
|
||||
if update_name {
|
||||
name.to_string()
|
||||
} else {
|
||||
@@ -694,7 +697,7 @@ impl Contact {
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
],
|
||||
),
|
||||
)?;
|
||||
|
||||
sth_modified = Modifier::Created;
|
||||
@@ -795,12 +798,12 @@ impl Contact {
|
||||
ORDER BY c.last_seen DESC, c.id DESC;",
|
||||
sql::repeat_vars(self_addrs.len())
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
|
||||
ContactId::LAST_SPECIAL,
|
||||
Origin::IncomingReplyTo,
|
||||
s3str_like_cmd,
|
||||
s3str_like_cmd,
|
||||
if flag_verified_only { 0i32 } else { 1i32 },
|
||||
if flag_verified_only { 0i32 } else { 1i32 }
|
||||
])),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
@@ -847,7 +850,7 @@ impl Contact {
|
||||
ORDER BY last_seen DESC, id DESC;",
|
||||
sql::repeat_vars(self_addrs.len())
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
|
||||
ContactId::LAST_SPECIAL,
|
||||
Origin::IncomingReplyTo
|
||||
])),
|
||||
@@ -880,7 +883,7 @@ impl Contact {
|
||||
.transaction(move |transaction| {
|
||||
let mut stmt = transaction
|
||||
.prepare("SELECT name, grpid FROM chats WHERE type=? AND blocked=?")?;
|
||||
let rows = stmt.query_map(params![Chattype::Mailinglist, Blocked::Yes], |row| {
|
||||
let rows = stmt.query_map((Chattype::Mailinglist, Blocked::Yes), |row| {
|
||||
let name: String = row.get(0)?;
|
||||
let grpid: String = row.get(1)?;
|
||||
Ok((name, grpid))
|
||||
@@ -902,7 +905,7 @@ impl Contact {
|
||||
// Always do an update in case the blocking is reset or name is changed.
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?",
|
||||
params![&name, Origin::MailinglistAddress, &grpid],
|
||||
(&name, Origin::MailinglistAddress, &grpid),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -917,7 +920,7 @@ impl Contact {
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
|
||||
paramsv![ContactId::LAST_SPECIAL],
|
||||
(ContactId::LAST_SPECIAL,),
|
||||
)
|
||||
.await?;
|
||||
Ok(count)
|
||||
@@ -933,7 +936,7 @@ impl Contact {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY last_seen DESC, id DESC;",
|
||||
paramsv![ContactId::LAST_SPECIAL],
|
||||
(ContactId::LAST_SPECIAL,),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
ids.collect::<std::result::Result<Vec<_>, _>>()
|
||||
@@ -1027,12 +1030,12 @@ impl Contact {
|
||||
let deleted_contacts = transaction.execute(
|
||||
"DELETE FROM contacts WHERE id=?
|
||||
AND (SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?)=0;",
|
||||
paramsv![contact_id, contact_id],
|
||||
(contact_id, contact_id),
|
||||
)?;
|
||||
if deleted_contacts == 0 {
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET origin=? WHERE id=?;",
|
||||
paramsv![Origin::Hidden, contact_id],
|
||||
(Origin::Hidden, contact_id),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -1060,7 +1063,7 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET param=? WHERE id=?",
|
||||
paramsv![self.param.to_string(), self.id],
|
||||
(self.param.to_string(), self.id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1072,7 +1075,7 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET status=? WHERE id=?",
|
||||
paramsv![self.status, self.id],
|
||||
(&self.status, self.id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1192,9 +1195,7 @@ impl Contact {
|
||||
if peerstate.verified_key.is_some() {
|
||||
return Ok(VerifiedStatus::BidirectVerified);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
|
||||
} else if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
|
||||
if peerstate.verified_key.is_some() {
|
||||
return Ok(VerifiedStatus::BidirectVerified);
|
||||
}
|
||||
@@ -1230,7 +1231,7 @@ impl Contact {
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM contacts WHERE id>?;",
|
||||
paramsv![ContactId::LAST_SPECIAL],
|
||||
(ContactId::LAST_SPECIAL,),
|
||||
)
|
||||
.await?;
|
||||
Ok(count)
|
||||
@@ -1244,10 +1245,7 @@ impl Contact {
|
||||
|
||||
let exists = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM contacts WHERE id=?;",
|
||||
paramsv![contact_id],
|
||||
)
|
||||
.exists("SELECT COUNT(*) FROM contacts WHERE id=?;", (contact_id,))
|
||||
.await?;
|
||||
Ok(exists)
|
||||
}
|
||||
@@ -1262,7 +1260,7 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
|
||||
paramsv![origin, contact_id, origin],
|
||||
(origin, contact_id, origin),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1291,18 +1289,20 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.is_empty() {
|
||||
captures
|
||||
.get(1)
|
||||
.map_or("".to_string(), |m| normalize_name(m.as_str()))
|
||||
strip_rtlo_characters(
|
||||
&captures
|
||||
.get(1)
|
||||
.map_or("".to_string(), |m| normalize_name(m.as_str())),
|
||||
)
|
||||
} else {
|
||||
name.to_string()
|
||||
strip_rtlo_characters(name)
|
||||
},
|
||||
captures
|
||||
.get(2)
|
||||
.map_or("".to_string(), |m| m.as_str().to_string()),
|
||||
)
|
||||
} else {
|
||||
(name.to_string(), addr.to_string())
|
||||
(strip_rtlo_characters(name), addr.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1324,7 +1324,7 @@ async fn set_block_contact(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET blocked=? WHERE id=?;",
|
||||
paramsv![i32::from(new_blocking), contact_id],
|
||||
(i32::from(new_blocking), contact_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1343,7 +1343,7 @@ WHERE type=? AND id IN (
|
||||
SELECT chat_id FROM chats_contacts WHERE contact_id=?
|
||||
);
|
||||
"#,
|
||||
paramsv![new_blocking, Chattype::Single, contact_id],
|
||||
(new_blocking, Chattype::Single, contact_id),
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -1460,13 +1460,16 @@ pub(crate) async fn update_last_seen(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2",
|
||||
paramsv![timestamp, contact_id],
|
||||
(timestamp, contact_id),
|
||||
)
|
||||
.await?
|
||||
> 0
|
||||
&& timestamp > time() - SEEN_RECENTLY_SECONDS
|
||||
{
|
||||
context.interrupt_recently_seen(contact_id, timestamp).await;
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_recently_seen(contact_id, timestamp)
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1486,7 +1489,7 @@ pub fn normalize_name(full_name: &str) -> String {
|
||||
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().into()),
|
||||
.map_or("".to_string(), |s| s.trim().to_string()),
|
||||
_ => full_name.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -1572,7 +1575,7 @@ impl RecentlySeenLoop {
|
||||
.query_map(
|
||||
"SELECT id, last_seen FROM contacts
|
||||
WHERE last_seen > ?",
|
||||
paramsv![time() - SEEN_RECENTLY_SECONDS],
|
||||
(time() - SEEN_RECENTLY_SECONDS,),
|
||||
|row| {
|
||||
let contact_id: ContactId = row.get("id")?;
|
||||
let last_seen: i64 = row.get("last_seen")?;
|
||||
|
||||
292
src/context.rs
292
src/context.rs
@@ -11,20 +11,19 @@ use std::time::{Duration, Instant, SystemTime};
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
use ratelimit::Ratelimit;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tokio::task;
|
||||
use tokio::sync::{Mutex, Notify, RwLock};
|
||||
|
||||
use crate::chat::{get_chat_cnt, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_VERSION_STR;
|
||||
use crate::contact::Contact;
|
||||
use crate::debug_logging::DebugEventLogData;
|
||||
use crate::debug_logging::DebugLogging;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
use crate::quota::QuotaInfo;
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::scheduler::SchedulerState;
|
||||
use crate::sql::Sql;
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::timesmearing::SmearedTimestamp;
|
||||
@@ -191,6 +190,10 @@ pub struct InnerContext {
|
||||
pub(crate) blobdir: PathBuf,
|
||||
pub(crate) sql: Sql,
|
||||
pub(crate) smeared_timestamp: SmearedTimestamp,
|
||||
/// The global "ongoing" process state.
|
||||
///
|
||||
/// This is a global mutex-like state for operations which should be modal in the
|
||||
/// clients.
|
||||
running_state: RwLock<RunningState>,
|
||||
/// Mutex to avoid generating the key for the user more than once.
|
||||
pub(crate) generating_key_mutex: Mutex<()>,
|
||||
@@ -201,7 +204,7 @@ pub struct InnerContext {
|
||||
pub(crate) translated_stockstrings: StockStrings,
|
||||
pub(crate) events: Events,
|
||||
|
||||
pub(crate) scheduler: RwLock<Option<Scheduler>>,
|
||||
pub(crate) scheduler: SchedulerState,
|
||||
pub(crate) ratelimit: RwLock<Ratelimit>,
|
||||
|
||||
/// Recently loaded quota information, if any.
|
||||
@@ -214,6 +217,11 @@ pub struct InnerContext {
|
||||
/// IMAP UID resync request.
|
||||
pub(crate) resync_request: AtomicBool,
|
||||
|
||||
/// Notify about new messages.
|
||||
///
|
||||
/// This causes [`Context::wait_next_msgs`] to wake up.
|
||||
pub(crate) new_msgs_notify: Notify,
|
||||
|
||||
/// Server ID response if ID capability is supported
|
||||
/// and the server returned non-NIL on the inbox connection.
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc2971>
|
||||
@@ -235,18 +243,10 @@ pub struct InnerContext {
|
||||
pub(crate) last_error: std::sync::RwLock<String>,
|
||||
|
||||
/// If debug logging is enabled, this contains all necessary information
|
||||
pub(crate) debug_logging: RwLock<Option<DebugLogging>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DebugLogging {
|
||||
/// The message containing the logging xdc
|
||||
pub(crate) msg_id: MsgId,
|
||||
/// Handle to the background task responsible for sending
|
||||
pub(crate) loop_handle: task::JoinHandle<()>,
|
||||
/// Channel that log events should be send to
|
||||
/// A background loop will receive and handle them
|
||||
pub(crate) sender: Sender<DebugEventLogData>,
|
||||
///
|
||||
/// Standard RwLock instead of [`tokio::sync::RwLock`] is used
|
||||
/// because the lock is used from synchronous [`Context::emit_event`].
|
||||
pub(crate) debug_logging: std::sync::RwLock<Option<DebugLogging>>,
|
||||
}
|
||||
|
||||
/// The state of ongoing process.
|
||||
@@ -256,7 +256,7 @@ enum RunningState {
|
||||
Running { cancel_sender: Sender<()> },
|
||||
|
||||
/// Cancel signal has been sent, waiting for ongoing process to be freed.
|
||||
ShallStop,
|
||||
ShallStop { request: Instant },
|
||||
|
||||
/// There is no ongoing process, a new one can be allocated.
|
||||
Stopped,
|
||||
@@ -359,6 +359,11 @@ impl Context {
|
||||
blobdir.display()
|
||||
);
|
||||
|
||||
let new_msgs_notify = Notify::new();
|
||||
// Notify once immediately to allow processing old messages
|
||||
// without starting I/O.
|
||||
new_msgs_notify.notify_one();
|
||||
|
||||
let inner = InnerContext {
|
||||
id,
|
||||
blobdir,
|
||||
@@ -370,16 +375,17 @@ impl Context {
|
||||
wrong_pw_warning_mutex: Mutex::new(()),
|
||||
translated_stockstrings: stockstrings,
|
||||
events,
|
||||
scheduler: RwLock::new(None),
|
||||
scheduler: SchedulerState::new(),
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
|
||||
quota: RwLock::new(None),
|
||||
quota_update_request: AtomicBool::new(false),
|
||||
resync_request: AtomicBool::new(false),
|
||||
new_msgs_notify,
|
||||
server_id: RwLock::new(None),
|
||||
creation_time: std::time::SystemTime::now(),
|
||||
last_full_folder_scan: Mutex::new(None),
|
||||
last_error: std::sync::RwLock::new("".to_string()),
|
||||
debug_logging: RwLock::new(None),
|
||||
debug_logging: std::sync::RwLock::new(None),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
@@ -395,42 +401,23 @@ impl Context {
|
||||
warn!(self, "can not start io on a context that is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
info!(self, "starting IO");
|
||||
let mut lock = self.inner.scheduler.write().await;
|
||||
if lock.is_none() {
|
||||
match Scheduler::start(self.clone()).await {
|
||||
Err(err) => error!(self, "Failed to start IO: {:#}", err),
|
||||
Ok(scheduler) => *lock = Some(scheduler),
|
||||
}
|
||||
}
|
||||
self.scheduler.start(self.clone()).await;
|
||||
}
|
||||
|
||||
/// Stops the IO scheduler.
|
||||
pub async fn stop_io(&self) {
|
||||
// Sending an event wakes up event pollers (get_next_event)
|
||||
// so the caller of stop_io() can arrange for proper termination.
|
||||
// For this, the caller needs to instruct the event poller
|
||||
// to terminate on receiving the next event and then call stop_io()
|
||||
// which will emit the below event(s)
|
||||
info!(self, "stopping IO");
|
||||
if let Some(debug_logging) = self.debug_logging.read().await.as_ref() {
|
||||
debug_logging.loop_handle.abort();
|
||||
}
|
||||
if let Some(scheduler) = self.inner.scheduler.write().await.take() {
|
||||
scheduler.stop(self).await;
|
||||
}
|
||||
self.scheduler.stop(self).await;
|
||||
}
|
||||
|
||||
/// Restarts the IO scheduler if it was running before
|
||||
/// when it is not running this is an no-op
|
||||
pub async fn restart_io_if_running(&self) {
|
||||
info!(self, "restarting IO");
|
||||
let is_running = { self.inner.scheduler.read().await.is_some() };
|
||||
if is_running {
|
||||
self.stop_io().await;
|
||||
self.start_io().await;
|
||||
}
|
||||
self.scheduler.restart(self).await;
|
||||
}
|
||||
|
||||
/// Indicate that the network likely has come back.
|
||||
pub async fn maybe_network(&self) {
|
||||
self.scheduler.maybe_network().await;
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying SQL instance.
|
||||
@@ -453,41 +440,18 @@ impl Context {
|
||||
|
||||
/// Emits a single event.
|
||||
pub fn emit_event(&self, event: EventType) {
|
||||
if self
|
||||
.debug_logging
|
||||
.try_read()
|
||||
.ok()
|
||||
.map(|inner| inner.is_some())
|
||||
== Some(true)
|
||||
{
|
||||
self.send_log_event(event.clone()).ok();
|
||||
};
|
||||
let lock = self.debug_logging.read().expect("RwLock is poisoned");
|
||||
if let Some(debug_logging) = &*lock {
|
||||
debug_logging.log_event(event.clone());
|
||||
}
|
||||
}
|
||||
self.events.emit(Event {
|
||||
id: self.id,
|
||||
typ: event,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn send_log_event(&self, event: EventType) -> anyhow::Result<()> {
|
||||
if let Ok(lock) = self.debug_logging.try_read() {
|
||||
if let Some(DebugLogging {
|
||||
msg_id: xdc_id,
|
||||
sender,
|
||||
..
|
||||
}) = &*lock
|
||||
{
|
||||
let event_data = DebugEventLogData {
|
||||
time: time(),
|
||||
msg_id: *xdc_id,
|
||||
event,
|
||||
};
|
||||
|
||||
sender.try_send(event_data).ok();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Emits a generic MsgsChanged event (without chat or message id)
|
||||
pub fn emit_msgs_changed_without_ids(&self) {
|
||||
self.emit_event(EventType::MsgsChanged {
|
||||
@@ -521,6 +485,13 @@ impl Context {
|
||||
|
||||
// Ongoing process allocation/free/check
|
||||
|
||||
/// Tries to acquire the global UI "ongoing" mutex.
|
||||
///
|
||||
/// This is for modal operations during which no other user actions are allowed. Only
|
||||
/// one such operation is allowed at any given time.
|
||||
///
|
||||
/// The return value is a cancel token, which will release the ongoing mutex when
|
||||
/// dropped.
|
||||
pub(crate) async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
|
||||
let mut s = self.running_state.write().await;
|
||||
ensure!(
|
||||
@@ -538,6 +509,9 @@ impl Context {
|
||||
|
||||
pub(crate) async fn free_ongoing(&self) {
|
||||
let mut s = self.running_state.write().await;
|
||||
if let RunningState::ShallStop { request } = *s {
|
||||
info!(self, "Ongoing stopped in {:?}", request.elapsed());
|
||||
}
|
||||
*s = RunningState::Stopped;
|
||||
}
|
||||
|
||||
@@ -550,9 +524,11 @@ impl Context {
|
||||
warn!(self, "could not cancel ongoing: {:#}", err);
|
||||
}
|
||||
info!(self, "Signaling the ongoing process to stop ASAP.",);
|
||||
*s = RunningState::ShallStop;
|
||||
*s = RunningState::ShallStop {
|
||||
request: Instant::now(),
|
||||
};
|
||||
}
|
||||
RunningState::ShallStop | RunningState::Stopped => {
|
||||
RunningState::ShallStop { .. } | RunningState::Stopped => {
|
||||
info!(self, "No ongoing process to stop.",);
|
||||
}
|
||||
}
|
||||
@@ -562,7 +538,7 @@ impl Context {
|
||||
pub(crate) async fn shall_stop_ongoing(&self) -> bool {
|
||||
match &*self.running_state.read().await {
|
||||
RunningState::Running { .. } => false,
|
||||
RunningState::ShallStop | RunningState::Stopped => true,
|
||||
RunningState::ShallStop { .. } | RunningState::Stopped => true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,6 +751,10 @@ impl Context {
|
||||
"debug_logging",
|
||||
self.get_config_int(Config::DebugLogging).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"last_msg_id",
|
||||
self.get_config_int(Config::LastMsgId).await?.to_string(),
|
||||
);
|
||||
|
||||
let elapsed = self.creation_time.elapsed();
|
||||
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
||||
@@ -807,7 +787,7 @@ impl Context {
|
||||
" AND NOT(c.muted_until=-1 OR c.muted_until>?)",
|
||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
||||
),
|
||||
paramsv![MessageState::InFresh, time()],
|
||||
(MessageState::InFresh, time()),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|rows| {
|
||||
let mut list = Vec::new();
|
||||
@@ -821,6 +801,66 @@ impl Context {
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Returns a list of messages with database ID higher than requested.
|
||||
///
|
||||
/// Blocked contacts and chats are excluded,
|
||||
/// but self-sent messages and contact requests are included in the results.
|
||||
pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> {
|
||||
let last_msg_id = match self.get_config(Config::LastMsgId).await? {
|
||||
Some(s) => MsgId::new(s.parse()?),
|
||||
None => MsgId::new_unset(),
|
||||
};
|
||||
|
||||
let list = self
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT m.id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
LEFT JOIN chats c
|
||||
ON m.chat_id=c.id
|
||||
WHERE m.id>?
|
||||
AND m.hidden=0
|
||||
AND m.chat_id>9
|
||||
AND ct.blocked=0
|
||||
AND c.blocked!=1
|
||||
ORDER BY m.id ASC",
|
||||
(
|
||||
last_msg_id.to_u32(), // Explicitly convert to u32 because 0 is allowed.
|
||||
),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
|rows| {
|
||||
let mut list = Vec::new();
|
||||
for row in rows {
|
||||
list.push(row?);
|
||||
}
|
||||
Ok(list)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Returns a list of messages with database ID higher than last marked as seen.
|
||||
///
|
||||
/// This function is supposed to be used by bot to request messages
|
||||
/// that are not processed yet.
|
||||
///
|
||||
/// Waits for notification and returns a result.
|
||||
/// Note that the result may be empty if the message is deleted
|
||||
/// shortly after notification or notification is manually triggered
|
||||
/// to interrupt waiting.
|
||||
/// Notification may be manually triggered by calling [`Self::stop_io`].
|
||||
pub async fn wait_next_msgs(&self) -> Result<Vec<MsgId>> {
|
||||
self.new_msgs_notify.notified().await;
|
||||
let list = self.get_next_msgs().await?;
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Searches for messages containing the query string.
|
||||
///
|
||||
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
|
||||
@@ -832,24 +872,10 @@ impl Context {
|
||||
}
|
||||
let str_like_in_text = format!("%{real_query}%");
|
||||
|
||||
let do_query = |query, params| {
|
||||
self.sql.query_map(
|
||||
query,
|
||||
params,
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for id in rows {
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let list = if let Some(chat_id) = chat_id {
|
||||
do_query(
|
||||
"SELECT m.id AS id
|
||||
self.sql
|
||||
.query_map(
|
||||
"SELECT m.id AS id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
@@ -858,9 +884,17 @@ impl Context {
|
||||
AND ct.blocked=0
|
||||
AND txt LIKE ?
|
||||
ORDER BY m.timestamp,m.id;",
|
||||
paramsv![chat_id, str_like_in_text],
|
||||
)
|
||||
.await?
|
||||
(chat_id, str_like_in_text),
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for id in rows {
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
// For performance reasons results are sorted only by `id`, that is in the order of
|
||||
// message reception.
|
||||
@@ -872,8 +906,9 @@ impl Context {
|
||||
// of unwanted results that are discarded moments later, we added `LIMIT 1000`.
|
||||
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
|
||||
// The limit is documented and UI may add a hint when getting 1000 results.
|
||||
do_query(
|
||||
"SELECT m.id AS id
|
||||
self.sql
|
||||
.query_map(
|
||||
"SELECT m.id AS id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
@@ -885,9 +920,17 @@ impl Context {
|
||||
AND ct.blocked=0
|
||||
AND m.txt LIKE ?
|
||||
ORDER BY m.id DESC LIMIT 1000",
|
||||
paramsv![str_like_in_text],
|
||||
)
|
||||
.await?
|
||||
(str_like_in_text,),
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for id in rows {
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok(list)
|
||||
@@ -1097,7 +1140,7 @@ mod tests {
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
paramsv![time() - 3600, bob.id],
|
||||
(time() - 3600, bob.id),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1114,10 +1157,7 @@ mod tests {
|
||||
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
|
||||
// that results in "muted forever" by definition.
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=-2 WHERE id=?;",
|
||||
paramsv![bob.id],
|
||||
)
|
||||
.execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,))
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
@@ -1452,4 +1492,38 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_next_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
let bob_next_msg_ids = bob.get_next_msgs().await?;
|
||||
assert_eq!(bob_next_msg_ids.len(), 1);
|
||||
assert_eq!(bob_next_msg_ids.get(0), Some(&received_msg.id));
|
||||
|
||||
bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32())
|
||||
.await?;
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
// Next messages include self-sent messages.
|
||||
let alice_next_msg_ids = alice.get_next_msgs().await?;
|
||||
assert_eq!(alice_next_msg_ids.len(), 1);
|
||||
assert_eq!(alice_next_msg_ids.get(0), Some(&sent_msg.sender_msg_id));
|
||||
|
||||
alice
|
||||
.set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32())
|
||||
.await?;
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,41 @@
|
||||
use crate::{
|
||||
chat::ChatId,
|
||||
config::Config,
|
||||
context::{Context, DebugLogging},
|
||||
context::Context,
|
||||
message::{Message, MsgId, Viewtype},
|
||||
param::Param,
|
||||
tools::time,
|
||||
webxdc::StatusUpdateItem,
|
||||
Event, EventType,
|
||||
EventType,
|
||||
};
|
||||
use async_channel::{self as channel, Receiver};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use tokio::task;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DebugLogging {
|
||||
/// The message containing the logging xdc
|
||||
pub(crate) msg_id: MsgId,
|
||||
/// Handle to the background task responsible for sending
|
||||
pub(crate) loop_handle: task::JoinHandle<()>,
|
||||
/// Channel that log events should be sent to.
|
||||
/// A background loop will receive and handle them.
|
||||
pub(crate) sender: Sender<DebugEventLogData>,
|
||||
}
|
||||
|
||||
impl DebugLogging {
|
||||
pub(crate) fn log_event(&self, event: EventType) {
|
||||
let event_data = DebugEventLogData {
|
||||
time: time(),
|
||||
msg_id: self.msg_id,
|
||||
event,
|
||||
};
|
||||
|
||||
self.sender.try_send(event_data).ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// Store all information needed to log an event to a webxdc.
|
||||
pub struct DebugEventLogData {
|
||||
pub time: i64,
|
||||
@@ -48,12 +72,9 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver<DebugEventLo
|
||||
eprintln!("Can't log event to webxdc status update: {err:#}");
|
||||
}
|
||||
Ok(serial) => {
|
||||
context.events.emit(Event {
|
||||
id: context.id,
|
||||
typ: EventType::WebxdcStatusUpdate {
|
||||
msg_id,
|
||||
status_update_serial: serial,
|
||||
},
|
||||
context.emit_event(EventType::WebxdcStatusUpdate {
|
||||
msg_id,
|
||||
status_update_serial: serial,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -112,24 +133,26 @@ pub(crate) async fn set_debug_logging_xdc(ctx: &Context, id: Option<MsgId>) -> a
|
||||
Some(msg_id.to_string().as_ref()),
|
||||
)
|
||||
.await?;
|
||||
let debug_logging = &mut *ctx.debug_logging.write().await;
|
||||
match debug_logging {
|
||||
// Switch logging xdc
|
||||
Some(debug_logging) => debug_logging.msg_id = msg_id,
|
||||
// Bootstrap background loop for message forwarding
|
||||
None => {
|
||||
let (sender, debug_logging_recv) = channel::bounded(1000);
|
||||
let loop_handle = {
|
||||
let ctx = ctx.clone();
|
||||
task::spawn(
|
||||
async move { debug_logging_loop(&ctx, debug_logging_recv).await },
|
||||
)
|
||||
};
|
||||
*debug_logging = Some(DebugLogging {
|
||||
msg_id,
|
||||
loop_handle,
|
||||
sender,
|
||||
});
|
||||
{
|
||||
let debug_logging = &mut *ctx.debug_logging.write().expect("RwLock is poisoned");
|
||||
match debug_logging {
|
||||
// Switch logging xdc
|
||||
Some(debug_logging) => debug_logging.msg_id = msg_id,
|
||||
// Bootstrap background loop for message forwarding
|
||||
None => {
|
||||
let (sender, debug_logging_recv) = channel::bounded(1000);
|
||||
let loop_handle = {
|
||||
let ctx = ctx.clone();
|
||||
task::spawn(async move {
|
||||
debug_logging_loop(&ctx, debug_logging_recv).await
|
||||
})
|
||||
};
|
||||
*debug_logging = Some(DebugLogging {
|
||||
msg_id,
|
||||
loop_handle,
|
||||
sender,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
info!(ctx, "replacing logging webxdc");
|
||||
@@ -139,7 +162,7 @@ pub(crate) async fn set_debug_logging_xdc(ctx: &Context, id: Option<MsgId>) -> a
|
||||
ctx.sql
|
||||
.set_raw_config(Config::DebugLogging.as_ref(), None)
|
||||
.await?;
|
||||
*ctx.debug_logging.write().await = None;
|
||||
*ctx.debug_logging.write().expect("RwLock is poisoned") = None;
|
||||
info!(ctx, "removing logging webxdc");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ impl MsgId {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET download_state=? WHERE id=?;",
|
||||
paramsv![download_state, self],
|
||||
(download_state, self),
|
||||
)
|
||||
.await?;
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
@@ -134,7 +134,7 @@ impl Job {
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target=folder",
|
||||
paramsv![msg.rfc724_mid],
|
||||
(&msg.rfc724_mid,),
|
||||
|row| {
|
||||
let server_uid: u32 = row.get(0)?;
|
||||
let server_folder: String = row.get(1)?;
|
||||
|
||||
@@ -174,10 +174,7 @@ impl ChatId {
|
||||
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer> {
|
||||
let timer = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT ephemeral_timer FROM chats WHERE id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
.query_get_value("SELECT ephemeral_timer FROM chats WHERE id=?;", (self,))
|
||||
.await?;
|
||||
Ok(timer.unwrap_or_default())
|
||||
}
|
||||
@@ -199,7 +196,7 @@ impl ChatId {
|
||||
"UPDATE chats
|
||||
SET ephemeral_timer=?
|
||||
WHERE id=?;",
|
||||
paramsv![timer, self],
|
||||
(timer, self),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -291,10 +288,7 @@ impl MsgId {
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> Result<Timer> {
|
||||
let res = match context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT ephemeral_timer FROM msgs WHERE id=?",
|
||||
paramsv![self],
|
||||
)
|
||||
.query_get_value("SELECT ephemeral_timer FROM msgs WHERE id=?", (self,))
|
||||
.await?
|
||||
{
|
||||
None | Some(0) => Timer::Disabled,
|
||||
@@ -314,10 +308,10 @@ impl MsgId {
|
||||
"UPDATE msgs SET ephemeral_timestamp = ? \
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
|
||||
AND id = ?",
|
||||
paramsv![ephemeral_timestamp, ephemeral_timestamp, self],
|
||||
(ephemeral_timestamp, ephemeral_timestamp, self),
|
||||
)
|
||||
.await?;
|
||||
context.interrupt_ephemeral_task().await;
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -338,14 +332,14 @@ pub(crate) async fn start_ephemeral_timers_msgids(
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
std::iter::once(&now as &dyn crate::ToSql)
|
||||
.chain(std::iter::once(&now as &dyn crate::ToSql))
|
||||
std::iter::once(&now as &dyn crate::sql::ToSql)
|
||||
.chain(std::iter::once(&now as &dyn crate::sql::ToSql))
|
||||
.chain(params_iter(msg_ids)),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
if count > 0 {
|
||||
context.interrupt_ephemeral_task().await;
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -369,7 +363,7 @@ WHERE
|
||||
AND ephemeral_timestamp <= ?
|
||||
AND chat_id != ?
|
||||
"#,
|
||||
paramsv![now, DC_CHAT_ID_TRASH],
|
||||
(now, DC_CHAT_ID_TRASH),
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
@@ -402,12 +396,12 @@ WHERE
|
||||
AND chat_id != ?
|
||||
AND chat_id != ?
|
||||
"#,
|
||||
paramsv![
|
||||
(
|
||||
threshold_timestamp,
|
||||
DC_CHAT_ID_LAST_SPECIAL,
|
||||
self_chat_id,
|
||||
device_chat_id
|
||||
],
|
||||
device_chat_id,
|
||||
),
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
@@ -449,7 +443,7 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
|
||||
SET chat_id=?, txt='', subject='', txt_raw='',
|
||||
mime_headers='', from_id=0, to_id=0, param=''
|
||||
WHERE id=?",
|
||||
params![DC_CHAT_ID_TRASH, msg_id],
|
||||
(DC_CHAT_ID_TRASH, msg_id),
|
||||
)?;
|
||||
|
||||
msgs_changed.push((chat_id, msg_id));
|
||||
@@ -494,7 +488,7 @@ async fn next_delete_device_after_timestamp(context: &Context) -> Result<Option<
|
||||
AND chat_id != ?
|
||||
AND chat_id != ?;
|
||||
"#,
|
||||
paramsv![DC_CHAT_ID_TRASH, self_chat_id, device_chat_id],
|
||||
(DC_CHAT_ID_TRASH, self_chat_id, device_chat_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -518,7 +512,7 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
|
||||
WHERE ephemeral_timestamp != 0
|
||||
AND chat_id != ?;
|
||||
"#,
|
||||
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
|
||||
(DC_CHAT_ID_TRASH,), // Trash contains already deleted messages, skip them
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -575,7 +569,8 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
|
||||
|
||||
delete_expired_messages(context, time())
|
||||
.await
|
||||
.ok_or_log(context);
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -604,12 +599,12 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()
|
||||
(download_state != 0 AND timestamp < ?) OR
|
||||
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
|
||||
)",
|
||||
paramsv![
|
||||
target,
|
||||
(
|
||||
&target,
|
||||
threshold_timestamp,
|
||||
threshold_timestamp_extended,
|
||||
now,
|
||||
],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -634,12 +629,12 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
|
||||
WHERE ephemeral_timer > 0 \
|
||||
AND ephemeral_timestamp = 0 \
|
||||
AND state NOT IN (?, ?, ?)",
|
||||
paramsv![
|
||||
(
|
||||
time(),
|
||||
MessageState::InFresh,
|
||||
MessageState::InNoticed,
|
||||
MessageState::OutDraft
|
||||
],
|
||||
MessageState::OutDraft,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1105,7 +1100,7 @@ mod tests {
|
||||
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
|
||||
let rawtxt: Option<String> = t
|
||||
.sql
|
||||
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id])
|
||||
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(rawtxt.is_none_or_empty(), "{rawtxt:?}");
|
||||
@@ -1130,13 +1125,13 @@ mod tests {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
paramsv![id, message_id, timestamp, ephemeral_timestamp],
|
||||
(id, &message_id, timestamp, ephemeral_timestamp),
|
||||
)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
|
||||
paramsv![message_id, id],
|
||||
(&message_id, id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1147,7 +1142,7 @@ mod tests {
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
paramsv![id.to_string()],
|
||||
(id.to_string(),),
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
@@ -1158,10 +1153,7 @@ mod tests {
|
||||
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM imap WHERE rfc724_mid=?",
|
||||
paramsv![id.to_string()],
|
||||
)
|
||||
.execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
262
src/events.rs
262
src/events.rs
@@ -1,15 +1,10 @@
|
||||
//! # Events specification.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use async_channel::{self as channel, Receiver, Sender, TrySendError};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::contact::ContactId;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::message::MsgId;
|
||||
use crate::webxdc::StatusUpdateSerial;
|
||||
mod payload;
|
||||
|
||||
pub use self::payload::EventType;
|
||||
|
||||
/// Event channel.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -32,7 +27,9 @@ impl Events {
|
||||
Self { receiver, sender }
|
||||
}
|
||||
|
||||
/// Emits an event.
|
||||
/// Emits an event into event channel.
|
||||
///
|
||||
/// If the channel is full, deletes the oldest event first.
|
||||
pub fn emit(&self, event: Event) {
|
||||
match self.sender.try_send(event) {
|
||||
Ok(()) => {}
|
||||
@@ -49,7 +46,7 @@ impl Events {
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the event emitter.
|
||||
/// Creates an event emitter.
|
||||
pub fn get_emitter(&self) -> EventEmitter {
|
||||
EventEmitter(self.receiver.clone())
|
||||
}
|
||||
@@ -108,248 +105,3 @@ pub struct Event {
|
||||
/// These are documented in `deltachat.h` as the `DC_EVENT_*` constants.
|
||||
pub typ: EventType,
|
||||
}
|
||||
|
||||
/// Event payload.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub enum EventType {
|
||||
/// The library-user may write an informational string to the log.
|
||||
///
|
||||
/// This event should *not* be reported to the end-user using a popup or something like
|
||||
/// that.
|
||||
Info(String),
|
||||
|
||||
/// Emitted when SMTP connection is established and login was successful.
|
||||
SmtpConnected(String),
|
||||
|
||||
/// Emitted when IMAP connection is established and login was successful.
|
||||
ImapConnected(String),
|
||||
|
||||
/// Emitted when a message was successfully sent to the SMTP server.
|
||||
SmtpMessageSent(String),
|
||||
|
||||
/// Emitted when an IMAP message has been marked as deleted
|
||||
ImapMessageDeleted(String),
|
||||
|
||||
/// Emitted when an IMAP message has been moved
|
||||
ImapMessageMoved(String),
|
||||
|
||||
/// Emitted when an new file in the $BLOBDIR was created
|
||||
NewBlobFile(String),
|
||||
|
||||
/// Emitted when an file in the $BLOBDIR was deleted
|
||||
DeletedBlobFile(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(String),
|
||||
|
||||
/// The library-user should report an error to the end-user.
|
||||
///
|
||||
/// As most things are asynchronous, things may go wrong at any time and the user
|
||||
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
|
||||
///
|
||||
/// However, for ongoing processes (eg. configure())
|
||||
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
|
||||
/// 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(String),
|
||||
|
||||
/// An action cannot be performed because the user is not in the group.
|
||||
/// Reported eg. after a call to
|
||||
/// dc_set_chat_name(), dc_set_chat_profile_image(),
|
||||
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
|
||||
/// dc_send_text_msg() or another sending function.
|
||||
ErrorSelfNotInGroup(String),
|
||||
|
||||
/// Messages or chats changed. One or more messages or chats changed for various
|
||||
/// reasons in the database:
|
||||
/// - Messages sent, received or removed
|
||||
/// - Chats created, deleted or archived
|
||||
/// - A draft has been set
|
||||
///
|
||||
MsgsChanged {
|
||||
/// Set if only a single chat is affected by the changes, otherwise 0.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// Set if only a single message is affected by the changes, otherwise 0.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// Reactions for the message changed.
|
||||
ReactionsChanged {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// ID of the message for which reactions were changed.
|
||||
msg_id: MsgId,
|
||||
|
||||
/// ID of the contact whose reaction set is changed.
|
||||
contact_id: ContactId,
|
||||
},
|
||||
|
||||
/// 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.
|
||||
IncomingMsg {
|
||||
/// ID of the chat where the message is assigned.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// ID of the message.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// Downloading a bunch of messages just finished.
|
||||
IncomingMsgBunch {
|
||||
/// List of incoming message IDs.
|
||||
msg_ids: Vec<MsgId>,
|
||||
},
|
||||
|
||||
/// Messages were seen or noticed.
|
||||
/// chat id is always set.
|
||||
MsgsNoticed(ChatId),
|
||||
|
||||
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
|
||||
MsgDelivered {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// ID of the message that was successfully sent.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// 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 dc_msg_get_state().
|
||||
MsgFailed {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// ID of the message that could not be sent.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
|
||||
MsgRead {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// ID of the message that was read.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
/// Or the verify state of a chat has changed.
|
||||
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
||||
/// and dc_remove_contact_from_chat().
|
||||
///
|
||||
/// This event does not include ephemeral timer modification, which
|
||||
/// is a separate event.
|
||||
ChatModified(ChatId),
|
||||
|
||||
/// Chat ephemeral timer changed.
|
||||
ChatEphemeralTimerModified {
|
||||
/// Chat ID.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// New ephemeral timer value.
|
||||
timer: EphemeralTimer,
|
||||
},
|
||||
|
||||
/// 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.
|
||||
ContactsChanged(Option<ContactId>),
|
||||
|
||||
/// Location of one or more contact has changed.
|
||||
///
|
||||
/// @param data1 (u32) contact_id of the contact for which the location has changed.
|
||||
/// If the locations of several contacts have been changed,
|
||||
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
|
||||
LocationChanged(Option<ContactId>),
|
||||
|
||||
/// Inform about the configuration progress started by configure().
|
||||
ConfigureProgress {
|
||||
/// Progress.
|
||||
///
|
||||
/// 0=error, 1-999=progress in permille, 1000=success and done
|
||||
progress: usize,
|
||||
|
||||
/// Progress comment or error, something to display to the user.
|
||||
comment: Option<String>,
|
||||
},
|
||||
|
||||
/// Inform about the import/export progress started by imex().
|
||||
///
|
||||
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||
/// @param data2 0
|
||||
ImexProgress(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().
|
||||
///
|
||||
/// A typical purpose for a handler of this event may be to make the file public to some system
|
||||
/// services.
|
||||
///
|
||||
/// @param data2 0
|
||||
ImexFileWritten(PathBuf),
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the inviter
|
||||
/// (Alice, the person who shows the QR code).
|
||||
///
|
||||
/// These events are typically sent after a joiner has scanned the QR code
|
||||
/// generated by dc_get_securejoin_qr().
|
||||
SecurejoinInviterProgress {
|
||||
/// ID of the contact that wants to join.
|
||||
contact_id: ContactId,
|
||||
|
||||
/// Progress as:
|
||||
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
|
||||
/// 1000=Protocol finished for this contact.
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the joiner
|
||||
/// (Bob, the person who scans the QR code).
|
||||
/// The events are typically sent while dc_join_securejoin(), which
|
||||
/// may take some time, is executed.
|
||||
SecurejoinJoinerProgress {
|
||||
/// ID of the inviting contact.
|
||||
contact_id: ContactId,
|
||||
|
||||
/// Progress as:
|
||||
/// 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)
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
/// The connectivity to the server changed.
|
||||
/// This means that you should refresh the connectivity view
|
||||
/// and possibly the connectivtiy HTML; see dc_get_connectivity() and
|
||||
/// dc_get_connectivity_html() for details.
|
||||
ConnectivityChanged,
|
||||
|
||||
/// The user's avatar changed.
|
||||
SelfavatarChanged,
|
||||
|
||||
/// Webxdc status update received.
|
||||
WebxdcStatusUpdate {
|
||||
/// Message ID.
|
||||
msg_id: MsgId,
|
||||
|
||||
/// Status update ID.
|
||||
status_update_serial: StatusUpdateSerial,
|
||||
},
|
||||
|
||||
/// Inform that a message containing a webxdc instance has been deleted.
|
||||
WebxdcInstanceDeleted {
|
||||
/// ID of the deleted message.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
}
|
||||
|
||||
258
src/events/payload.rs
Normal file
258
src/events/payload.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
//! # Event payloads.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::contact::ContactId;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::message::MsgId;
|
||||
use crate::webxdc::StatusUpdateSerial;
|
||||
|
||||
/// Event payload.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum EventType {
|
||||
/// The library-user may write an informational string to the log.
|
||||
///
|
||||
/// This event should *not* be reported to the end-user using a popup or something like
|
||||
/// that.
|
||||
Info(String),
|
||||
|
||||
/// Emitted when SMTP connection is established and login was successful.
|
||||
SmtpConnected(String),
|
||||
|
||||
/// Emitted when IMAP connection is established and login was successful.
|
||||
ImapConnected(String),
|
||||
|
||||
/// Emitted when a message was successfully sent to the SMTP server.
|
||||
SmtpMessageSent(String),
|
||||
|
||||
/// Emitted when an IMAP message has been marked as deleted
|
||||
ImapMessageDeleted(String),
|
||||
|
||||
/// Emitted when an IMAP message has been moved
|
||||
ImapMessageMoved(String),
|
||||
|
||||
/// Emitted before going into IDLE on the Inbox folder.
|
||||
ImapInboxIdle,
|
||||
|
||||
/// Emitted when an new file in the $BLOBDIR was created
|
||||
NewBlobFile(String),
|
||||
|
||||
/// Emitted when an file in the $BLOBDIR was deleted
|
||||
DeletedBlobFile(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(String),
|
||||
|
||||
/// The library-user should report an error to the end-user.
|
||||
///
|
||||
/// As most things are asynchronous, things may go wrong at any time and the user
|
||||
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
|
||||
///
|
||||
/// However, for ongoing processes (eg. configure())
|
||||
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
|
||||
/// 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(String),
|
||||
|
||||
/// An action cannot be performed because the user is not in the group.
|
||||
/// Reported eg. after a call to
|
||||
/// dc_set_chat_name(), dc_set_chat_profile_image(),
|
||||
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
|
||||
/// dc_send_text_msg() or another sending function.
|
||||
ErrorSelfNotInGroup(String),
|
||||
|
||||
/// Messages or chats changed. One or more messages or chats changed for various
|
||||
/// reasons in the database:
|
||||
/// - Messages sent, received or removed
|
||||
/// - Chats created, deleted or archived
|
||||
/// - A draft has been set
|
||||
///
|
||||
MsgsChanged {
|
||||
/// Set if only a single chat is affected by the changes, otherwise 0.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// Set if only a single message is affected by the changes, otherwise 0.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// Reactions for the message changed.
|
||||
ReactionsChanged {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// ID of the message for which reactions were changed.
|
||||
msg_id: MsgId,
|
||||
|
||||
/// ID of the contact whose reaction set is changed.
|
||||
contact_id: ContactId,
|
||||
},
|
||||
|
||||
/// 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.
|
||||
IncomingMsg {
|
||||
/// ID of the chat where the message is assigned.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// ID of the message.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// Downloading a bunch of messages just finished.
|
||||
IncomingMsgBunch {
|
||||
/// List of incoming message IDs.
|
||||
msg_ids: Vec<MsgId>,
|
||||
},
|
||||
|
||||
/// Messages were seen or noticed.
|
||||
/// chat id is always set.
|
||||
MsgsNoticed(ChatId),
|
||||
|
||||
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
|
||||
MsgDelivered {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// ID of the message that was successfully sent.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// 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 dc_msg_get_state().
|
||||
MsgFailed {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// ID of the message that could not be sent.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
|
||||
MsgRead {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// ID of the message that was read.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
/// Or the verify state of a chat has changed.
|
||||
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
||||
/// and dc_remove_contact_from_chat().
|
||||
///
|
||||
/// This event does not include ephemeral timer modification, which
|
||||
/// is a separate event.
|
||||
ChatModified(ChatId),
|
||||
|
||||
/// Chat ephemeral timer changed.
|
||||
ChatEphemeralTimerModified {
|
||||
/// Chat ID.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// New ephemeral timer value.
|
||||
timer: EphemeralTimer,
|
||||
},
|
||||
|
||||
/// 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.
|
||||
ContactsChanged(Option<ContactId>),
|
||||
|
||||
/// Location of one or more contact has changed.
|
||||
///
|
||||
/// @param data1 (u32) contact_id of the contact for which the location has changed.
|
||||
/// If the locations of several contacts have been changed,
|
||||
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
|
||||
LocationChanged(Option<ContactId>),
|
||||
|
||||
/// Inform about the configuration progress started by configure().
|
||||
ConfigureProgress {
|
||||
/// Progress.
|
||||
///
|
||||
/// 0=error, 1-999=progress in permille, 1000=success and done
|
||||
progress: usize,
|
||||
|
||||
/// Progress comment or error, something to display to the user.
|
||||
comment: Option<String>,
|
||||
},
|
||||
|
||||
/// Inform about the import/export progress started by imex().
|
||||
///
|
||||
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||
/// @param data2 0
|
||||
ImexProgress(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().
|
||||
///
|
||||
/// A typical purpose for a handler of this event may be to make the file public to some system
|
||||
/// services.
|
||||
///
|
||||
/// @param data2 0
|
||||
ImexFileWritten(PathBuf),
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the inviter
|
||||
/// (Alice, the person who shows the QR code).
|
||||
///
|
||||
/// These events are typically sent after a joiner has scanned the QR code
|
||||
/// generated by dc_get_securejoin_qr().
|
||||
SecurejoinInviterProgress {
|
||||
/// ID of the contact that wants to join.
|
||||
contact_id: ContactId,
|
||||
|
||||
/// Progress as:
|
||||
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
|
||||
/// 1000=Protocol finished for this contact.
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the joiner
|
||||
/// (Bob, the person who scans the QR code).
|
||||
/// The events are typically sent while dc_join_securejoin(), which
|
||||
/// may take some time, is executed.
|
||||
SecurejoinJoinerProgress {
|
||||
/// ID of the inviting contact.
|
||||
contact_id: ContactId,
|
||||
|
||||
/// Progress as:
|
||||
/// 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)
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
/// The connectivity to the server changed.
|
||||
/// This means that you should refresh the connectivity view
|
||||
/// and possibly the connectivtiy HTML; see dc_get_connectivity() and
|
||||
/// dc_get_connectivity_html() for details.
|
||||
ConnectivityChanged,
|
||||
|
||||
/// The user's avatar changed.
|
||||
SelfavatarChanged,
|
||||
|
||||
/// Webxdc status update received.
|
||||
WebxdcStatusUpdate {
|
||||
/// Message ID.
|
||||
msg_id: MsgId,
|
||||
|
||||
/// Status update ID.
|
||||
status_update_serial: StatusUpdateSerial,
|
||||
},
|
||||
|
||||
/// Inform that a message containing a webxdc instance has been deleted.
|
||||
WebxdcInstanceDeleted {
|
||||
/// ID of the deleted message.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
//! `MsgId.get_html()` will return HTML -
|
||||
//! this allows nice quoting, handling linebreaks properly etc.
|
||||
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
|
||||
24
src/http.rs
24
src/http.rs
@@ -1,24 +0,0 @@
|
||||
//! # HTTP module.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::socks::Socks5Config;
|
||||
|
||||
const HTTP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
pub(crate) fn get_client(socks5_config: Option<Socks5Config>) -> Result<reqwest::Client> {
|
||||
let builder = reqwest::ClientBuilder::new().timeout(HTTP_TIMEOUT);
|
||||
let builder = if let Some(socks5_config) = socks5_config {
|
||||
let proxy = reqwest::Proxy::all(socks5_config.to_url())?;
|
||||
builder.proxy(proxy)
|
||||
} else {
|
||||
// Disable usage of "system" proxy configured via environment variables.
|
||||
// It is enabled by default in `reqwest`, see
|
||||
// <https://docs.rs/reqwest/0.11.14/reqwest/struct.ClientBuilder.html#method.no_proxy>
|
||||
// for documentation.
|
||||
builder.no_proxy()
|
||||
};
|
||||
Ok(builder.build()?)
|
||||
}
|
||||
95
src/imap.rs
95
src/imap.rs
@@ -14,7 +14,7 @@ use std::{
|
||||
use anyhow::{bail, format_err, Context as _, Result};
|
||||
use async_channel::Receiver;
|
||||
use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
|
||||
use futures::StreamExt;
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::chat::{self, ChatId, ChatIdBlocked};
|
||||
@@ -475,7 +475,7 @@ impl Imap {
|
||||
// Note that the `Config::DeleteDeviceAfter` timer starts as soon as the messages are
|
||||
// fetched while the per-chat ephemeral timers start as soon as the messages are marked
|
||||
// as noticed.
|
||||
context.interrupt_ephemeral_task().await;
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
|
||||
let session = self
|
||||
@@ -516,8 +516,7 @@ impl Imap {
|
||||
.uid_fetch("1:*", RFC724MID_UID)
|
||||
.await
|
||||
.with_context(|| format!("can't resync folder {folder}"))?;
|
||||
while let Some(fetch) = list.next().await {
|
||||
let fetch = fetch?;
|
||||
while let Some(fetch) = list.try_next().await? {
|
||||
let headers = match get_fetch_headers(&fetch) {
|
||||
Ok(headers) => headers,
|
||||
Err(err) => {
|
||||
@@ -551,7 +550,7 @@ impl Imap {
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
transaction.execute("DELETE FROM imap WHERE folder=?", params![folder])?;
|
||||
transaction.execute("DELETE FROM imap WHERE folder=?", (folder,))?;
|
||||
for (uid, (rfc724_mid, target)) in &msgs {
|
||||
// This may detect previously undetected moved
|
||||
// messages, so we update server_folder too.
|
||||
@@ -561,7 +560,7 @@ impl Imap {
|
||||
ON CONFLICT(folder, uid, uidvalidity)
|
||||
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
|
||||
target=excluded.target",
|
||||
params![rfc724_mid, folder, uid, uid_validity, target],
|
||||
(rfc724_mid, folder, uid, uid_validity, target),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -660,7 +659,7 @@ impl Imap {
|
||||
.context("Error fetching UID")?;
|
||||
|
||||
let mut new_last_seen_uid = None;
|
||||
while let Some(fetch) = list.next().await.transpose()? {
|
||||
while let Some(fetch) = list.try_next().await? {
|
||||
if fetch.message == mailbox.exists && fetch.uid.is_some() {
|
||||
new_last_seen_uid = fetch.uid;
|
||||
}
|
||||
@@ -677,7 +676,7 @@ impl Imap {
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM imap WHERE folder=? AND uidvalidity!=?",
|
||||
paramsv![folder, new_uid_validity],
|
||||
(&folder, new_uid_validity),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -760,7 +759,7 @@ impl Imap {
|
||||
ON CONFLICT(folder, uid, uidvalidity)
|
||||
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
|
||||
target=excluded.target",
|
||||
paramsv![message_id, folder, uid, uid_validity, &target],
|
||||
(&message_id, &folder, uid, uid_validity, &target),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1051,7 +1050,7 @@ impl Session {
|
||||
WHERE folder = ?
|
||||
AND target != folder
|
||||
ORDER BY target, uid",
|
||||
paramsv![folder],
|
||||
(folder,),
|
||||
|row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let uid: u32 = row.get(1)?;
|
||||
@@ -1198,8 +1197,11 @@ impl Imap {
|
||||
.await
|
||||
.context("failed to fetch flags")?;
|
||||
|
||||
while let Some(fetch) = list.next().await {
|
||||
let fetch = fetch.context("failed to get FETCH result")?;
|
||||
while let Some(fetch) = list
|
||||
.try_next()
|
||||
.await
|
||||
.context("failed to get FETCH result")?
|
||||
{
|
||||
let uid = if let Some(uid) = fetch.uid {
|
||||
uid
|
||||
} else {
|
||||
@@ -1258,8 +1260,7 @@ impl Imap {
|
||||
.await
|
||||
.context("IMAP Could not fetch")?;
|
||||
|
||||
while let Some(fetch) = list.next().await {
|
||||
let msg = fetch?;
|
||||
while let Some(msg) = list.try_next().await? {
|
||||
match get_fetch_headers(&msg) {
|
||||
Ok(headers) => {
|
||||
if let Some(from) = mimeparser::get_from(&headers) {
|
||||
@@ -1294,8 +1295,7 @@ impl Imap {
|
||||
.context("IMAP could not fetch")?;
|
||||
|
||||
let mut msgs = BTreeMap::new();
|
||||
while let Some(fetch) = list.next().await {
|
||||
let msg = fetch?;
|
||||
while let Some(msg) = list.try_next().await? {
|
||||
if let Some(msg_uid) = msg.uid {
|
||||
// If the mailbox is not empty, results always include
|
||||
// at least one UID, even if last_seen_uid+1 is past
|
||||
@@ -1332,8 +1332,7 @@ impl Imap {
|
||||
.context("IMAP Could not fetch")?;
|
||||
|
||||
let mut msgs = BTreeMap::new();
|
||||
while let Some(fetch) = list.next().await {
|
||||
let msg = fetch?;
|
||||
while let Some(msg) = list.try_next().await? {
|
||||
if let Some(msg_uid) = msg.uid {
|
||||
msgs.insert((msg.internal_date(), msg_uid), msg);
|
||||
}
|
||||
@@ -1680,8 +1679,7 @@ impl Imap {
|
||||
let mut delimiter_is_default = true;
|
||||
let mut folder_configs = BTreeMap::new();
|
||||
|
||||
while let Some(folder) = folders.next().await {
|
||||
let folder = folder?;
|
||||
while let Some(folder) = folders.try_next().await? {
|
||||
info!(context, "Scanning folder: {:?}", folder);
|
||||
|
||||
// Update the delimiter iff there is a different one, but only once.
|
||||
@@ -1742,17 +1740,37 @@ impl Session {
|
||||
/// If this returns `true`, this means that new emails arrived and you should
|
||||
/// fetch again, even if you just fetched.
|
||||
fn server_sent_unsolicited_exists(&self, context: &Context) -> Result<bool> {
|
||||
use async_imap::imap_proto::Response;
|
||||
use async_imap::imap_proto::ResponseCode;
|
||||
use UnsolicitedResponse::*;
|
||||
|
||||
let mut unsolicited_exists = false;
|
||||
while let Ok(response) = self.unsolicited_responses.try_recv() {
|
||||
match response {
|
||||
UnsolicitedResponse::Exists(_) => {
|
||||
Exists(_) => {
|
||||
info!(
|
||||
context,
|
||||
"Need to fetch again, got unsolicited EXISTS {:?}", response
|
||||
);
|
||||
unsolicited_exists = true;
|
||||
}
|
||||
_ => info!(context, "ignoring unsolicited response {:?}", response),
|
||||
|
||||
// We are not interested in the following responses and they are are
|
||||
// sent quite frequently, so, we ignore them without logging them
|
||||
Expunge(_) | Recent(_) => {}
|
||||
Other(response_data)
|
||||
if matches!(
|
||||
response_data.parsed(),
|
||||
Response::Fetch { .. }
|
||||
| Response::Done {
|
||||
code: Some(ResponseCode::CopyUid(_, _, _)),
|
||||
..
|
||||
}
|
||||
) => {}
|
||||
|
||||
_ => {
|
||||
info!(context, "got unsolicited response {:?}", response)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(unsolicited_exists)
|
||||
@@ -2170,7 +2188,7 @@ async fn mark_seen_by_uid(
|
||||
AND uid=?3
|
||||
LIMIT 1
|
||||
)",
|
||||
paramsv![&folder, uid_validity, uid],
|
||||
(&folder, uid_validity, uid),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let chat_id: ChatId = row.get(1)?;
|
||||
@@ -2186,12 +2204,12 @@ async fn mark_seen_by_uid(
|
||||
"UPDATE msgs SET state=?1
|
||||
WHERE (state=?2 OR state=?3)
|
||||
AND id=?4",
|
||||
paramsv![
|
||||
(
|
||||
MessageState::InSeen,
|
||||
MessageState::InFresh,
|
||||
MessageState::InNoticed,
|
||||
msg_id
|
||||
],
|
||||
msg_id,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to update msg {msg_id} state"))?
|
||||
@@ -2221,10 +2239,13 @@ pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str)
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO imap_markseen (id)
|
||||
SELECT id FROM imap WHERE rfc724_mid=?",
|
||||
paramsv![message_id],
|
||||
(message_id,),
|
||||
)
|
||||
.await?;
|
||||
context.interrupt_inbox(InterruptInfo::new(false)).await;
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_inbox(InterruptInfo::new(false))
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2238,7 +2259,7 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
|
||||
.execute(
|
||||
"INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
|
||||
ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
|
||||
paramsv![folder, uid_next],
|
||||
(folder, uid_next),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -2252,10 +2273,7 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
|
||||
async fn get_uid_next(context: &Context, folder: &str) -> Result<u32> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT uid_next FROM imap_sync WHERE folder=?;",
|
||||
paramsv![folder],
|
||||
)
|
||||
.query_get_value("SELECT uid_next FROM imap_sync WHERE folder=?;", (folder,))
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
@@ -2270,7 +2288,7 @@ pub(crate) async fn set_uidvalidity(
|
||||
.execute(
|
||||
"INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
|
||||
ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
|
||||
paramsv![folder, uidvalidity],
|
||||
(folder, uidvalidity),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -2281,7 +2299,7 @@ async fn get_uidvalidity(context: &Context, folder: &str) -> Result<u32> {
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT uidvalidity FROM imap_sync WHERE folder=?;",
|
||||
paramsv![folder],
|
||||
(folder,),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
@@ -2293,7 +2311,7 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) ->
|
||||
.execute(
|
||||
"INSERT INTO imap_sync (folder, modseq) VALUES (?,?)
|
||||
ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq",
|
||||
paramsv![folder, modseq],
|
||||
(folder, modseq),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -2302,10 +2320,7 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) ->
|
||||
async fn get_modseq(context: &Context, folder: &str) -> Result<u64> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT modseq FROM imap_sync WHERE folder=?;",
|
||||
paramsv![folder],
|
||||
)
|
||||
.query_get_value("SELECT modseq FROM imap_sync WHERE folder=?;", (folder,))
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
use anyhow::{Context as _, Result};
|
||||
use async_imap::Client as ImapClient;
|
||||
use async_imap::Session as ImapSession;
|
||||
use tokio::io::BufStream;
|
||||
use tokio::io::BufWriter;
|
||||
|
||||
use super::capabilities::Capabilities;
|
||||
use super::session::Session;
|
||||
@@ -96,7 +96,7 @@ impl Client {
|
||||
) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?;
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream).await?;
|
||||
let buffered_stream = BufStream::new(tls_stream);
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = ImapClient::new(session_stream);
|
||||
|
||||
@@ -110,7 +110,7 @@ impl Client {
|
||||
|
||||
pub async fn connect_insecure(context: &Context, hostname: &str, port: u16) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, false).await?;
|
||||
let buffered_stream = BufStream::new(tcp_stream);
|
||||
let buffered_stream = BufWriter::new(tcp_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = ImapClient::new(session_stream);
|
||||
let _greeting = client
|
||||
@@ -145,7 +145,7 @@ impl Client {
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
|
||||
let buffered_stream = BufStream::new(tls_stream);
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let client = ImapClient::new(session_stream);
|
||||
|
||||
@@ -163,7 +163,7 @@ impl Client {
|
||||
.connect(context, domain, port, IMAP_TIMEOUT, strict_tls)
|
||||
.await?;
|
||||
let tls_stream = wrap_tls(strict_tls, domain, socks5_stream).await?;
|
||||
let buffered_stream = BufStream::new(tls_stream);
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = ImapClient::new(session_stream);
|
||||
let _greeting = client
|
||||
@@ -183,7 +183,7 @@ impl Client {
|
||||
let socks5_stream = socks5_config
|
||||
.connect(context, domain, port, IMAP_TIMEOUT, false)
|
||||
.await?;
|
||||
let buffered_stream = BufStream::new(socks5_stream);
|
||||
let buffered_stream = BufWriter::new(socks5_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = ImapClient::new(session_stream);
|
||||
let _greeting = client
|
||||
@@ -220,7 +220,7 @@ impl Client {
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, socks5_stream)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
let buffered_stream = BufStream::new(tls_stream);
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let client = ImapClient::new(session_stream);
|
||||
|
||||
|
||||
@@ -70,7 +70,9 @@ impl Imap {
|
||||
loop {
|
||||
self.fetch_move_delete(context, folder.name(), folder_meaning)
|
||||
.await
|
||||
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
|
||||
.context("Can't fetch new msgs in scanned folder")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
let session = self.session.as_mut().context("no session")?;
|
||||
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
|
||||
@@ -105,7 +107,11 @@ impl Imap {
|
||||
let list = session
|
||||
.list(Some(""), Some("*"))
|
||||
.await?
|
||||
.filter_map(|f| async { f.ok_or_log_msg(context, "list_folders() can't get folder") });
|
||||
.filter_map(|f| async {
|
||||
f.context("list_folders() can't get folder")
|
||||
.log_err(context)
|
||||
.ok()
|
||||
});
|
||||
Ok(list.collect().await)
|
||||
}
|
||||
}
|
||||
|
||||
138
src/imex.rs
138
src/imex.rs
@@ -5,18 +5,19 @@ use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use ::pgp::types::KeyTrait;
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Error, Result};
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use futures::StreamExt;
|
||||
use futures_lite::FutureExt;
|
||||
use rand::{thread_rng, Rng};
|
||||
use tokio::fs::{self, File};
|
||||
use tokio_tar::Archive;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::blob::{BlobDirContents, BlobObject};
|
||||
use crate::chat::{self, delete_and_reset_all_device_msgs, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::e2ee;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
|
||||
use crate::log::LogExt;
|
||||
@@ -30,7 +31,10 @@ use crate::tools::{
|
||||
create_folder, delete_file, get_filesuffix_lc, open_file_std, read_file, time, write_file,
|
||||
EmailAddress,
|
||||
};
|
||||
use crate::{e2ee, tools};
|
||||
|
||||
mod transfer;
|
||||
|
||||
pub use transfer::{get_backup, BackupProvider};
|
||||
|
||||
// Name of the database file in the backup.
|
||||
const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
|
||||
@@ -86,13 +90,15 @@ pub async fn imex(
|
||||
) -> Result<()> {
|
||||
let cancel = context.alloc_ongoing().await?;
|
||||
|
||||
let res = imex_inner(context, what, path, passphrase)
|
||||
.race(async {
|
||||
cancel.recv().await.ok();
|
||||
Err(format_err!("canceled"))
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = {
|
||||
let _guard = context.scheduler.pause(context.clone()).await?;
|
||||
imex_inner(context, what, path, passphrase)
|
||||
.race(async {
|
||||
cancel.recv().await.ok();
|
||||
Err(format_err!("canceled"))
|
||||
})
|
||||
.await
|
||||
};
|
||||
context.free_ongoing().await;
|
||||
|
||||
if let Err(err) = res.as_ref() {
|
||||
@@ -250,7 +256,7 @@ async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
|
||||
msg.text = Some(
|
||||
"It seems you are using multiple devices with Delta Chat. Great!\n\n\
|
||||
If you also want to synchronize outgoing messages across all devices, \
|
||||
go to the settings and enable \"Send copy to self\"."
|
||||
go to \"Settings → Advanced\" and enable \"Send Copy to Self\"."
|
||||
.to_string(),
|
||||
);
|
||||
chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg)).await?;
|
||||
@@ -391,8 +397,7 @@ async fn imex_inner(
|
||||
export_backup(context, path, passphrase.unwrap_or_default()).await
|
||||
}
|
||||
ImexMode::ImportBackup => {
|
||||
import_backup(context, path, passphrase.unwrap_or_default()).await?;
|
||||
context.sql.run_migrations(context).await
|
||||
import_backup(context, path, passphrase.unwrap_or_default()).await
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,7 +418,7 @@ async fn import_backup(
|
||||
"Cannot import backups to accounts in use."
|
||||
);
|
||||
ensure!(
|
||||
context.scheduler.read().await.is_none(),
|
||||
!context.scheduler.is_running().await,
|
||||
"cannot import backup, IO is running"
|
||||
);
|
||||
|
||||
@@ -468,6 +473,7 @@ async fn import_backup(
|
||||
}
|
||||
}
|
||||
|
||||
context.sql.run_migrations(context).await?;
|
||||
delete_and_reset_all_device_msgs(context).await?;
|
||||
|
||||
Ok(())
|
||||
@@ -516,16 +522,9 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res
|
||||
let _d1 = DeleteOnDrop(temp_db_path.clone());
|
||||
let _d2 = DeleteOnDrop(temp_path.clone());
|
||||
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int("backup_time", now as i32)
|
||||
.await?;
|
||||
sql::housekeeping(context).await.ok_or_log(context);
|
||||
|
||||
ensure!(
|
||||
context.scheduler.read().await.is_none(),
|
||||
"cannot export backup, IO is running"
|
||||
);
|
||||
export_database(context, &temp_db_path, passphrase)
|
||||
.await
|
||||
.context("could not export database")?;
|
||||
|
||||
info!(
|
||||
context,
|
||||
@@ -534,32 +533,6 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res
|
||||
dest_path.display(),
|
||||
);
|
||||
|
||||
let path_str = temp_db_path
|
||||
.to_str()
|
||||
.with_context(|| format!("path {temp_db_path:?} is not valid unicode"))?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.call_write(|conn| {
|
||||
if let Err(err) = conn.execute("VACUUM", params![]) {
|
||||
info!(context, "Vacuum failed, exporting anyway: {:#}.", err);
|
||||
}
|
||||
conn.execute(
|
||||
"ATTACH DATABASE ? AS backup KEY ?",
|
||||
paramsv![path_str, passphrase],
|
||||
)
|
||||
.context("failed to attach backup database")?;
|
||||
let res = conn
|
||||
.query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
|
||||
.context("failed to export to attached backup database");
|
||||
conn.execute("DETACH DATABASE backup", [])
|
||||
.context("failed to detach backup database")?;
|
||||
res?;
|
||||
|
||||
Ok::<_, Error>(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
let res = export_backup_inner(context, &temp_db_path, &temp_path).await;
|
||||
|
||||
match &res {
|
||||
@@ -597,29 +570,15 @@ async fn export_backup_inner(
|
||||
.append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME)
|
||||
.await?;
|
||||
|
||||
let read_dir = tools::read_dir(context.get_blobdir()).await?;
|
||||
let count = read_dir.len();
|
||||
let mut written_files = 0;
|
||||
|
||||
let blobdir = BlobDirContents::new(context).await?;
|
||||
let mut last_progress = 0;
|
||||
for entry in read_dir {
|
||||
let name = entry.file_name();
|
||||
if !entry.file_type().await?.is_file() {
|
||||
warn!(
|
||||
context,
|
||||
"Export: Found dir entry {} that is not a file, ignoring",
|
||||
name.to_string_lossy()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let mut file = File::open(entry.path()).await?;
|
||||
let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(name);
|
||||
builder.append_file(path_in_archive, &mut file).await?;
|
||||
|
||||
written_files += 1;
|
||||
let progress = 1000 * written_files / count;
|
||||
for (i, blob) in blobdir.iter().enumerate() {
|
||||
let mut file = File::open(blob.to_abs_path()).await?;
|
||||
let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(blob.as_name());
|
||||
builder.append_file(path_in_archive, &mut file).await?;
|
||||
let progress = 1000 * i / blobdir.len();
|
||||
if progress != last_progress && progress > 10 && progress < 1000 {
|
||||
// We already emitted ImexProgress(10) above
|
||||
context.emit_event(EventType::ImexProgress(progress));
|
||||
last_progress = progress;
|
||||
}
|
||||
@@ -781,6 +740,45 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Exports the database to *dest*, encrypted using *passphrase*.
|
||||
///
|
||||
/// The directory of *dest* must already exist, if *dest* itself exists it will be
|
||||
/// overwritten.
|
||||
///
|
||||
/// This also verifies that IO is not running during the export.
|
||||
async fn export_database(context: &Context, dest: &Path, passphrase: String) -> Result<()> {
|
||||
ensure!(
|
||||
!context.scheduler.is_running().await,
|
||||
"cannot export backup, IO is running"
|
||||
);
|
||||
let now = time().try_into().context("32-bit UNIX time overflow")?;
|
||||
|
||||
// TODO: Maybe introduce camino crate for UTF-8 paths where we need them.
|
||||
let dest = dest
|
||||
.to_str()
|
||||
.with_context(|| format!("path {} is not valid unicode", dest.display()))?;
|
||||
|
||||
context.sql.set_raw_config_int("backup_time", now).await?;
|
||||
sql::housekeeping(context).await.log_err(context).ok();
|
||||
context
|
||||
.sql
|
||||
.call_write(|conn| {
|
||||
conn.execute("VACUUM;", ())
|
||||
.map_err(|err| warn!(context, "Vacuum failed, exporting anyway {err}"))
|
||||
.ok();
|
||||
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (dest, passphrase))
|
||||
.context("failed to attach backup database")?;
|
||||
let res = conn
|
||||
.query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
|
||||
.context("failed to export to attached backup database");
|
||||
conn.execute("DETACH DATABASE backup", [])
|
||||
.context("failed to detach backup database")?;
|
||||
res?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
695
src/imex/transfer.rs
Normal file
695
src/imex/transfer.rs
Normal file
@@ -0,0 +1,695 @@
|
||||
//! Transfer a backup to an other device.
|
||||
//!
|
||||
//! This module provides support for using n0's iroh tool to initiate transfer of a backup
|
||||
//! to another device using a QR code.
|
||||
//!
|
||||
//! Using the iroh terminology there are two parties to this:
|
||||
//!
|
||||
//! - The *Provider*, which starts a server and listens for connections.
|
||||
//! - The *Getter*, which connects to the server and retrieves the data.
|
||||
//!
|
||||
//! Iroh is designed around the idea of verifying hashes, the downloads are verified as
|
||||
//! they are retrieved. The entire transfer is initiated by requesting the data of a single
|
||||
//! root hash.
|
||||
//!
|
||||
//! Both the provider and the getter are authenticated:
|
||||
//!
|
||||
//! - The provider is known by its *peer ID*.
|
||||
//! - The provider needs an *authentication token* from the getter before it accepts a
|
||||
//! connection.
|
||||
//!
|
||||
//! Both these are transferred in the QR code offered to the getter. This ensures that the
|
||||
//! getter can not connect to an impersonated provider and the provider does not offer the
|
||||
//! download to an impersonated getter.
|
||||
|
||||
use std::future::Future;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::task::Poll;
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
|
||||
use async_channel::Receiver;
|
||||
use futures_lite::StreamExt;
|
||||
use iroh::blobs::Collection;
|
||||
use iroh::get::DataStream;
|
||||
use iroh::progress::ProgressEmitter;
|
||||
use iroh::protocol::AuthToken;
|
||||
use iroh::provider::{DataSource, Event, Provider, Ticket};
|
||||
use iroh::Hash;
|
||||
use tokio::fs::{self, File};
|
||||
use tokio::io::{self, AsyncWriteExt, BufWriter};
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use tokio::task::{JoinHandle, JoinSet};
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::blob::BlobDirContents;
|
||||
use crate::chat::{add_device_msg, delete_and_reset_all_device_msgs};
|
||||
use crate::context::Context;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::qr::{self, Qr};
|
||||
use crate::stock_str::backup_transfer_msg_body;
|
||||
use crate::{e2ee, EventType};
|
||||
|
||||
use super::{export_database, DBFILE_BACKUP_NAME};
|
||||
|
||||
const MAX_CONCURRENT_DIALS: u8 = 16;
|
||||
|
||||
/// Provide or send a backup of this device.
|
||||
///
|
||||
/// This creates a backup of the current device and starts a service which offers another
|
||||
/// device to download this backup.
|
||||
///
|
||||
/// This does not make a full backup on disk, only the SQLite database is created on disk,
|
||||
/// the blobs in the blob directory are not copied.
|
||||
///
|
||||
/// This starts a task which acquires the global "ongoing" mutex. If you need to stop the
|
||||
/// task use the [`Context::stop_ongoing`] mechanism.
|
||||
///
|
||||
/// The task implements [`Future`] and awaiting it will complete once a transfer has been
|
||||
/// either completed or aborted.
|
||||
#[derive(Debug)]
|
||||
pub struct BackupProvider {
|
||||
/// The supervisor task, run by [`BackupProvider::watch_provider`].
|
||||
handle: JoinHandle<Result<()>>,
|
||||
/// The ticket to retrieve the backup collection.
|
||||
ticket: Ticket,
|
||||
/// Guard to cancel the provider on drop.
|
||||
_drop_guard: tokio_util::sync::DropGuard,
|
||||
}
|
||||
|
||||
impl BackupProvider {
|
||||
/// Prepares for sending a backup to a second device.
|
||||
///
|
||||
/// Before calling this function all I/O must be stopped so that no changes to the blobs
|
||||
/// or database are happening, this is done by calling the [`Accounts::stop_io`] or
|
||||
/// [`Context::stop_io`] APIs first.
|
||||
///
|
||||
/// This will acquire the global "ongoing process" mutex, which can be used to cancel
|
||||
/// the process.
|
||||
///
|
||||
/// [`Accounts::stop_io`]: crate::accounts::Accounts::stop_io
|
||||
pub async fn prepare(context: &Context) -> Result<Self> {
|
||||
e2ee::ensure_secret_key_exists(context)
|
||||
.await
|
||||
.context("Private key not available, aborting backup export")?;
|
||||
|
||||
// Acquire global "ongoing" mutex.
|
||||
let cancel_token = context.alloc_ongoing().await?;
|
||||
let paused_guard = context.scheduler.pause(context.clone()).await?;
|
||||
let context_dir = context
|
||||
.get_blobdir()
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("Context dir not found"))?;
|
||||
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
|
||||
if fs::metadata(&dbfile).await.is_ok() {
|
||||
fs::remove_file(&dbfile).await?;
|
||||
warn!(context, "Previous database export deleted");
|
||||
}
|
||||
let dbfile = TempPathGuard::new(dbfile);
|
||||
let res = tokio::select! {
|
||||
biased;
|
||||
res = Self::prepare_inner(context, &dbfile) => {
|
||||
match res {
|
||||
Ok(slf) => Ok(slf),
|
||||
Err(err) => {
|
||||
error!(context, "Failed to set up second device setup: {:#}", err);
|
||||
Err(err)
|
||||
},
|
||||
}
|
||||
},
|
||||
_ = cancel_token.recv() => Err(format_err!("cancelled")),
|
||||
};
|
||||
let (provider, ticket) = match res {
|
||||
Ok((provider, ticket)) => (provider, ticket),
|
||||
Err(err) => {
|
||||
context.free_ongoing().await;
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
let drop_token = CancellationToken::new();
|
||||
let handle = {
|
||||
let context = context.clone();
|
||||
let drop_token = drop_token.clone();
|
||||
tokio::spawn(async move {
|
||||
let res = Self::watch_provider(&context, provider, cancel_token, drop_token).await;
|
||||
context.free_ongoing().await;
|
||||
|
||||
// Explicit drop to move the guards into this future
|
||||
drop(paused_guard);
|
||||
drop(dbfile);
|
||||
res
|
||||
})
|
||||
};
|
||||
Ok(Self {
|
||||
handle,
|
||||
ticket,
|
||||
_drop_guard: drop_token.drop_guard(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates the provider task.
|
||||
///
|
||||
/// Having this as a function makes it easier to cancel it when needed.
|
||||
async fn prepare_inner(context: &Context, dbfile: &Path) -> Result<(Provider, Ticket)> {
|
||||
// Generate the token up front: we also use it to encrypt the database.
|
||||
let token = AuthToken::generate();
|
||||
context.emit_event(SendProgress::Started.into());
|
||||
export_database(context, dbfile, token.to_string())
|
||||
.await
|
||||
.context("Database export failed")?;
|
||||
context.emit_event(SendProgress::DatabaseExported.into());
|
||||
|
||||
// Now we can be sure IO is not running.
|
||||
let mut files = vec![DataSource::with_name(
|
||||
dbfile.to_owned(),
|
||||
format!("db/{DBFILE_BACKUP_NAME}"),
|
||||
)];
|
||||
let blobdir = BlobDirContents::new(context).await?;
|
||||
for blob in blobdir.iter() {
|
||||
let path = blob.to_abs_path();
|
||||
let name = format!("blob/{}", blob.as_file_name());
|
||||
files.push(DataSource::with_name(path, name));
|
||||
}
|
||||
|
||||
// Start listening.
|
||||
let (db, hash) = iroh::provider::create_collection(files).await?;
|
||||
context.emit_event(SendProgress::CollectionCreated.into());
|
||||
let provider = Provider::builder(db)
|
||||
.bind_addr((Ipv4Addr::UNSPECIFIED, 0).into())
|
||||
.auth_token(token)
|
||||
.spawn()?;
|
||||
context.emit_event(SendProgress::ProviderListening.into());
|
||||
info!(context, "Waiting for remote to connect");
|
||||
let ticket = provider.ticket(hash)?;
|
||||
Ok((provider, ticket))
|
||||
}
|
||||
|
||||
/// Supervises the iroh [`Provider`], terminating it when needed.
|
||||
///
|
||||
/// This will watch the provider and terminate it when:
|
||||
///
|
||||
/// - A transfer is completed, successful or unsuccessful.
|
||||
/// - An event could not be observed to protect against not knowing of a completed event.
|
||||
/// - The ongoing process is cancelled.
|
||||
///
|
||||
/// The *cancel_token* is the handle for the ongoing process mutex, when this completes
|
||||
/// we must cancel this operation.
|
||||
async fn watch_provider(
|
||||
context: &Context,
|
||||
mut provider: Provider,
|
||||
cancel_token: Receiver<()>,
|
||||
drop_token: CancellationToken,
|
||||
) -> Result<()> {
|
||||
let mut events = provider.subscribe();
|
||||
let mut total_size = 0;
|
||||
let mut current_size = 0;
|
||||
let res = loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
res = &mut provider => {
|
||||
break res.context("BackupProvider failed");
|
||||
},
|
||||
maybe_event = events.recv() => {
|
||||
match maybe_event {
|
||||
Ok(event) => {
|
||||
match event {
|
||||
Event::ClientConnected { ..} => {
|
||||
context.emit_event(SendProgress::ClientConnected.into());
|
||||
}
|
||||
Event::RequestReceived { .. } => {
|
||||
}
|
||||
Event::TransferCollectionStarted { total_blobs_size, .. } => {
|
||||
total_size = total_blobs_size;
|
||||
context.emit_event(SendProgress::TransferInProgress {
|
||||
current_size,
|
||||
total_size,
|
||||
}.into());
|
||||
}
|
||||
Event::TransferBlobCompleted { size, .. } => {
|
||||
current_size += size;
|
||||
context.emit_event(SendProgress::TransferInProgress {
|
||||
current_size,
|
||||
total_size,
|
||||
}.into());
|
||||
}
|
||||
Event::TransferCollectionCompleted { .. } => {
|
||||
context.emit_event(SendProgress::TransferInProgress {
|
||||
current_size: total_size,
|
||||
total_size
|
||||
}.into());
|
||||
provider.shutdown();
|
||||
}
|
||||
Event::TransferAborted { .. } => {
|
||||
provider.shutdown();
|
||||
break Err(anyhow!("BackupProvider transfer aborted"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
// We should never see this, provider.join() should complete
|
||||
// first.
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => {
|
||||
// We really shouldn't be lagging, if we did we may have missed
|
||||
// a completion event.
|
||||
provider.shutdown();
|
||||
break Err(anyhow!("Missed events from BackupProvider"));
|
||||
}
|
||||
}
|
||||
},
|
||||
_ = cancel_token.recv() => {
|
||||
provider.shutdown();
|
||||
break Err(anyhow!("BackupProvider cancelled"));
|
||||
},
|
||||
_ = drop_token.cancelled() => {
|
||||
provider.shutdown();
|
||||
break Err(anyhow!("BackupProvider dropped"));
|
||||
}
|
||||
}
|
||||
};
|
||||
match &res {
|
||||
Ok(_) => {
|
||||
context.emit_event(SendProgress::Completed.into());
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(backup_transfer_msg_body(context).await);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "Backup transfer failure: {err:#}");
|
||||
context.emit_event(SendProgress::Failed.into())
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// Returns a QR code that allows fetching this backup.
|
||||
///
|
||||
/// This QR code can be passed to [`get_backup`] on a (different) device.
|
||||
pub fn qr(&self) -> Qr {
|
||||
Qr::Backup {
|
||||
ticket: self.ticket.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for BackupProvider {
|
||||
type Output = Result<()>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.handle).poll(cx)?
|
||||
}
|
||||
}
|
||||
|
||||
/// A guard which will remove the path when dropped.
|
||||
///
|
||||
/// It implements [`Deref`] it it can be used as a `&Path`.
|
||||
#[derive(Debug)]
|
||||
struct TempPathGuard {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TempPathGuard {
|
||||
fn new(path: PathBuf) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempPathGuard {
|
||||
fn drop(&mut self) {
|
||||
let path = self.path.clone();
|
||||
tokio::spawn(async move {
|
||||
fs::remove_file(&path).await.ok();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TempPathGuard {
|
||||
type Target = Path;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
/// Create [`EventType::ImexProgress`] events using readable names.
|
||||
///
|
||||
/// Plus you get warnings if you don't use all variants.
|
||||
#[derive(Debug)]
|
||||
enum SendProgress {
|
||||
Failed,
|
||||
Started,
|
||||
DatabaseExported,
|
||||
CollectionCreated,
|
||||
ProviderListening,
|
||||
ClientConnected,
|
||||
TransferInProgress { current_size: u64, total_size: u64 },
|
||||
Completed,
|
||||
}
|
||||
|
||||
impl From<SendProgress> for EventType {
|
||||
fn from(source: SendProgress) -> Self {
|
||||
use SendProgress::*;
|
||||
let num: u16 = match source {
|
||||
Failed => 0,
|
||||
Started => 100,
|
||||
DatabaseExported => 300,
|
||||
CollectionCreated => 350,
|
||||
ProviderListening => 400,
|
||||
ClientConnected => 450,
|
||||
TransferInProgress {
|
||||
current_size,
|
||||
total_size,
|
||||
} => {
|
||||
// the range is 450..=950
|
||||
450 + ((current_size as f64 / total_size as f64) * 500.).floor() as u16
|
||||
}
|
||||
Completed => 1000,
|
||||
};
|
||||
Self::ImexProgress(num.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Contacts a backup provider and receives the backup from it.
|
||||
///
|
||||
/// This uses a QR code to contact another instance of deltachat which is providing a backup
|
||||
/// using the [`BackupProvider`]. Once connected it will authenticate using the secrets in
|
||||
/// the QR code and retrieve the backup.
|
||||
///
|
||||
/// This is a long running operation which will only when completed.
|
||||
///
|
||||
/// Using [`Qr`] as argument is a bit odd as it only accepts one specific variant of it. It
|
||||
/// does avoid having [`iroh::provider::Ticket`] in the primary API however, without
|
||||
/// having to revert to untyped bytes.
|
||||
pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
ensure!(
|
||||
matches!(qr, Qr::Backup { .. }),
|
||||
"QR code for backup must be of type DCBACKUP"
|
||||
);
|
||||
ensure!(
|
||||
!context.is_configured().await?,
|
||||
"Cannot import backups to accounts in use."
|
||||
);
|
||||
// Acquire global "ongoing" mutex.
|
||||
let cancel_token = context.alloc_ongoing().await?;
|
||||
let _guard = context.scheduler.pause(context.clone()).await;
|
||||
info!(
|
||||
context,
|
||||
"Running get_backup for {}",
|
||||
qr::format_backup(&qr)?
|
||||
);
|
||||
let res = tokio::select! {
|
||||
biased;
|
||||
res = get_backup_inner(context, qr) => res,
|
||||
_ = cancel_token.recv() => Err(format_err!("cancelled")),
|
||||
};
|
||||
context.free_ongoing().await;
|
||||
res
|
||||
}
|
||||
|
||||
async fn get_backup_inner(context: &Context, qr: Qr) -> Result<()> {
|
||||
let ticket = match qr {
|
||||
Qr::Backup { ticket } => ticket,
|
||||
_ => bail!("QR code for backup must be of type DCBACKUP"),
|
||||
};
|
||||
|
||||
match transfer_from_provider(context, &ticket).await {
|
||||
Ok(()) => {
|
||||
context.sql.run_migrations(context).await?;
|
||||
delete_and_reset_all_device_msgs(context).await?;
|
||||
context.emit_event(ReceiveProgress::Completed.into());
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
// Clean up any blobs we already wrote.
|
||||
let readdir = fs::read_dir(context.get_blobdir()).await?;
|
||||
let mut readdir = ReadDirStream::new(readdir);
|
||||
while let Some(dirent) = readdir.next().await {
|
||||
if let Ok(dirent) = dirent {
|
||||
fs::remove_file(dirent.path()).await.ok();
|
||||
}
|
||||
}
|
||||
context.emit_event(ReceiveProgress::Failed.into());
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn transfer_from_provider(context: &Context, ticket: &Ticket) -> Result<()> {
|
||||
let progress = ProgressEmitter::new(0, ReceiveProgress::max_blob_progress());
|
||||
spawn_progress_proxy(context.clone(), progress.subscribe());
|
||||
let on_connected = || {
|
||||
context.emit_event(ReceiveProgress::Connected.into());
|
||||
async { Ok(()) }
|
||||
};
|
||||
let on_collection = |collection: &Collection| {
|
||||
context.emit_event(ReceiveProgress::CollectionReceived.into());
|
||||
progress.set_total(collection.total_blobs_size());
|
||||
async { Ok(()) }
|
||||
};
|
||||
let jobs = Mutex::new(JoinSet::default());
|
||||
let on_blob =
|
||||
|hash, reader, name| on_blob(context, &progress, &jobs, ticket, hash, reader, name);
|
||||
|
||||
// Perform the transfer.
|
||||
let keylog = false; // Do not enable rustls SSLKEYLOGFILE env var functionality
|
||||
let stats = iroh::get::run_ticket(
|
||||
ticket,
|
||||
keylog,
|
||||
MAX_CONCURRENT_DIALS,
|
||||
on_connected,
|
||||
on_collection,
|
||||
on_blob,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut jobs = jobs.lock().await;
|
||||
while let Some(job) = jobs.join_next().await {
|
||||
job.context("job failed")?;
|
||||
}
|
||||
drop(progress);
|
||||
info!(
|
||||
context,
|
||||
"Backup transfer finished, transfer rate was {} Mbps.",
|
||||
stats.mbits()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get callback when a blob is received from the provider.
|
||||
///
|
||||
/// This writes the blobs to the blobdir. If the blob is the database it will import it to
|
||||
/// the database of the current [`Context`].
|
||||
async fn on_blob(
|
||||
context: &Context,
|
||||
progress: &ProgressEmitter,
|
||||
jobs: &Mutex<JoinSet<()>>,
|
||||
ticket: &Ticket,
|
||||
_hash: Hash,
|
||||
mut reader: DataStream,
|
||||
name: String,
|
||||
) -> Result<DataStream> {
|
||||
ensure!(!name.is_empty(), "Received a nameless blob");
|
||||
let path = if name.starts_with("db/") {
|
||||
let context_dir = context
|
||||
.get_blobdir()
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("Context dir not found"))?;
|
||||
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
|
||||
if fs::metadata(&dbfile).await.is_ok() {
|
||||
fs::remove_file(&dbfile).await?;
|
||||
warn!(context, "Previous database export deleted");
|
||||
}
|
||||
dbfile
|
||||
} else {
|
||||
ensure!(name.starts_with("blob/"), "malformatted blob name");
|
||||
let blobname = name.rsplit('/').next().context("malformatted blob name")?;
|
||||
context.get_blobdir().join(blobname)
|
||||
};
|
||||
|
||||
let mut wrapped_reader = progress.wrap_async_read(&mut reader);
|
||||
let file = File::create(&path).await?;
|
||||
let mut file = BufWriter::with_capacity(128 * 1024, file);
|
||||
io::copy(&mut wrapped_reader, &mut file).await?;
|
||||
file.flush().await?;
|
||||
|
||||
if name.starts_with("db/") {
|
||||
let context = context.clone();
|
||||
let token = ticket.token().to_string();
|
||||
jobs.lock().await.spawn(async move {
|
||||
if let Err(err) = context.sql.import(&path, token).await {
|
||||
error!(context, "cannot import database: {:#?}", err);
|
||||
}
|
||||
if let Err(err) = fs::remove_file(&path).await {
|
||||
error!(
|
||||
context,
|
||||
"failed to delete database import file '{}': {:#?}",
|
||||
path.display(),
|
||||
err,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(reader)
|
||||
}
|
||||
|
||||
/// Spawns a task proxying progress events.
|
||||
///
|
||||
/// This spawns a tokio task which receives events from the [`ProgressEmitter`] and sends
|
||||
/// them to the context. The task finishes when the emitter is dropped.
|
||||
///
|
||||
/// This could be done directly in the emitter by making it less generic.
|
||||
fn spawn_progress_proxy(context: Context, mut rx: broadcast::Receiver<u16>) {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(step) => context.emit_event(ReceiveProgress::BlobProgress(step).into()),
|
||||
Err(RecvError::Closed) => break,
|
||||
Err(RecvError::Lagged(_)) => continue,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Create [`EventType::ImexProgress`] events using readable names.
|
||||
///
|
||||
/// Plus you get warnings if you don't use all variants.
|
||||
#[derive(Debug)]
|
||||
enum ReceiveProgress {
|
||||
Connected,
|
||||
CollectionReceived,
|
||||
/// A value between 0 and 85 interpreted as a percentage.
|
||||
///
|
||||
/// Other values are already used by the other variants of this enum.
|
||||
BlobProgress(u16),
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl ReceiveProgress {
|
||||
/// The maximum value for [`ReceiveProgress::BlobProgress`].
|
||||
///
|
||||
/// This only exists to keep this magic value local in this type.
|
||||
fn max_blob_progress() -> u16 {
|
||||
85
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReceiveProgress> for EventType {
|
||||
fn from(source: ReceiveProgress) -> Self {
|
||||
let val = match source {
|
||||
ReceiveProgress::Connected => 50,
|
||||
ReceiveProgress::CollectionReceived => 100,
|
||||
ReceiveProgress::BlobProgress(val) => 100 + 10 * val,
|
||||
ReceiveProgress::Completed => 1000,
|
||||
ReceiveProgress::Failed => 0,
|
||||
};
|
||||
EventType::ImexProgress(val.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::chat::{get_chat_msgs, send_msg, ChatItem};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::test_utils::TestContextManager;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_receive() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
// Create first device.
|
||||
let ctx0 = tcm.alice().await;
|
||||
|
||||
// Write a message in the self chat
|
||||
let self_chat = ctx0.get_self_chat().await;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hi there".to_string()));
|
||||
send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
|
||||
|
||||
// Send an attachment in the self chat
|
||||
let file = ctx0.get_blobdir().join("hello.txt");
|
||||
fs::write(&file, "i am attachment").await.unwrap();
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), Some("text/plain"));
|
||||
send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
|
||||
|
||||
// Prepare to transfer backup.
|
||||
let provider = BackupProvider::prepare(&ctx0).await.unwrap();
|
||||
|
||||
// Set up second device.
|
||||
let ctx1 = tcm.unconfigured().await;
|
||||
get_backup(&ctx1, provider.qr()).await.unwrap();
|
||||
|
||||
// Make sure the provider finishes without an error.
|
||||
tokio::time::timeout(Duration::from_secs(30), provider)
|
||||
.await
|
||||
.expect("timed out")
|
||||
.expect("error in provider");
|
||||
|
||||
// Check that we have the self message.
|
||||
let self_chat = ctx1.get_self_chat().await;
|
||||
let msgs = get_chat_msgs(&ctx1, self_chat.id).await.unwrap();
|
||||
assert_eq!(msgs.len(), 2);
|
||||
let msgid = match msgs.get(0).unwrap() {
|
||||
ChatItem::Message { msg_id } => msg_id,
|
||||
_ => panic!("wrong chat item"),
|
||||
};
|
||||
let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap();
|
||||
let text = msg.get_text().unwrap();
|
||||
assert_eq!(text, "hi there");
|
||||
let msgid = match msgs.get(1).unwrap() {
|
||||
ChatItem::Message { msg_id } => msg_id,
|
||||
_ => panic!("wrong chat item"),
|
||||
};
|
||||
let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap();
|
||||
let path = msg.get_file(&ctx1).unwrap();
|
||||
let text = fs::read_to_string(&path).await.unwrap();
|
||||
assert_eq!(text, "i am attachment");
|
||||
|
||||
// Check that both received the ImexProgress events.
|
||||
ctx0.evtracker
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
ctx1.evtracker
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_progress() {
|
||||
let cases = [
|
||||
((0, 100), 450),
|
||||
((10, 100), 500),
|
||||
((50, 100), 700),
|
||||
((100, 100), 950),
|
||||
];
|
||||
|
||||
for ((current_size, total_size), progress) in cases {
|
||||
let out = EventType::from(SendProgress::TransferInProgress {
|
||||
current_size,
|
||||
total_size,
|
||||
});
|
||||
assert_eq!(out, EventType::ImexProgress(progress));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_drop_provider() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let ctx = tcm.alice().await;
|
||||
|
||||
let provider = BackupProvider::prepare(&ctx).await.unwrap();
|
||||
drop(provider);
|
||||
ctx.evtracker
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(0)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
65
src/job.rs
65
src/job.rs
@@ -99,7 +99,7 @@ impl Job {
|
||||
if self.job_id != 0 {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32])
|
||||
.execute("DELETE FROM jobs WHERE id=?;", (self.job_id as i32,))
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -117,22 +117,22 @@ impl Job {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE jobs SET desired_timestamp=?, tries=? WHERE id=?;",
|
||||
paramsv![
|
||||
(
|
||||
self.desired_timestamp,
|
||||
i64::from(self.tries),
|
||||
self.job_id as i32,
|
||||
],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
context.sql.execute(
|
||||
"INSERT INTO jobs (added_timestamp, action, foreign_id, desired_timestamp) VALUES (?,?,?,?);",
|
||||
paramsv![
|
||||
(
|
||||
self.added_timestamp,
|
||||
self.action,
|
||||
self.foreign_id,
|
||||
self.desired_timestamp
|
||||
]
|
||||
)
|
||||
).await?;
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ impl<'a> Connection<'a> {
|
||||
}
|
||||
|
||||
pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_>, mut job: Job) {
|
||||
info!(context, "job {} started...", &job);
|
||||
info!(context, "Job {} started...", &job);
|
||||
|
||||
let try_res = match perform_job_action(context, &mut job, &mut connection, 0).await {
|
||||
Status::RetryNow => perform_job_action(context, &mut job, &mut connection, 1).await,
|
||||
@@ -165,7 +165,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
|
||||
let tries = job.tries + 1;
|
||||
|
||||
if tries < JOB_RETRIES {
|
||||
info!(context, "increase job {} tries to {}", job, tries);
|
||||
info!(context, "Increase job {job} tries to {tries}.");
|
||||
job.tries = tries;
|
||||
let time_offset = get_backoff_time_offset(tries);
|
||||
job.desired_timestamp = time() + time_offset;
|
||||
@@ -177,26 +177,23 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
|
||||
time_offset
|
||||
);
|
||||
job.save(context).await.unwrap_or_else(|err| {
|
||||
error!(context, "failed to save job: {:#}", err);
|
||||
error!(context, "Failed to save job: {err:#}.");
|
||||
});
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"remove job {} as it exhausted {} retries", job, JOB_RETRIES
|
||||
"Remove job {job} as it exhausted {JOB_RETRIES} retries."
|
||||
);
|
||||
job.delete(context).await.unwrap_or_else(|err| {
|
||||
error!(context, "failed to delete job: {:#}", err);
|
||||
error!(context, "Failed to delete job: {err:#}.");
|
||||
});
|
||||
}
|
||||
}
|
||||
Status::Finished(res) => {
|
||||
if let Err(err) = res {
|
||||
warn!(
|
||||
context,
|
||||
"remove job {} as it failed with error {:#}", job, err
|
||||
);
|
||||
warn!(context, "Remove job {job} as it failed with error {err:#}.");
|
||||
} else {
|
||||
info!(context, "remove job {} as it succeeded", job);
|
||||
info!(context, "Remove job {job} as it succeeded.");
|
||||
}
|
||||
|
||||
job.delete(context).await.unwrap_or_else(|err| {
|
||||
@@ -212,13 +209,13 @@ async fn perform_job_action(
|
||||
connection: &mut Connection<'_>,
|
||||
tries: u32,
|
||||
) -> Status {
|
||||
info!(context, "begin immediate try {} of job {}", tries, job);
|
||||
info!(context, "Begin immediate try {tries} of job {job}.");
|
||||
|
||||
let try_res = match job.action {
|
||||
Action::DownloadMsg => job.download_msg(context, connection.inbox()).await,
|
||||
};
|
||||
|
||||
info!(context, "Finished immediate try {} of job {}", tries, job);
|
||||
info!(context, "Finished immediate try {tries} of job {job}.");
|
||||
|
||||
try_res
|
||||
}
|
||||
@@ -238,6 +235,7 @@ fn get_backoff_time_offset(tries: u32) -> i64 {
|
||||
pub(crate) async fn schedule_resync(context: &Context) -> Result<()> {
|
||||
context.resync_request.store(true, Ordering::Relaxed);
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_inbox(InterruptInfo {
|
||||
probe_network: false,
|
||||
})
|
||||
@@ -249,8 +247,11 @@ pub(crate) async fn schedule_resync(context: &Context) -> Result<()> {
|
||||
pub async fn add(context: &Context, job: Job) -> Result<()> {
|
||||
job.save(context).await.context("failed to save job")?;
|
||||
|
||||
info!(context, "interrupt: imap");
|
||||
context.interrupt_inbox(InterruptInfo::new(false)).await;
|
||||
info!(context, "Interrupt: IMAP.");
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_inbox(InterruptInfo::new(false))
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -260,7 +261,7 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
|
||||
/// jobs, this is tricky and probably wrong currently. Look at the
|
||||
/// SQL queries for details.
|
||||
pub(crate) async fn load_next(context: &Context, info: &InterruptInfo) -> Result<Option<Job>> {
|
||||
info!(context, "loading job");
|
||||
info!(context, "Loading job.");
|
||||
|
||||
let query;
|
||||
let params;
|
||||
@@ -276,7 +277,7 @@ WHERE desired_timestamp<=?
|
||||
ORDER BY action DESC, added_timestamp
|
||||
LIMIT 1;
|
||||
"#;
|
||||
params = paramsv![t];
|
||||
params = vec![t];
|
||||
} else {
|
||||
// processing after call to dc_maybe_network():
|
||||
// process _all_ pending jobs that failed before
|
||||
@@ -288,13 +289,13 @@ WHERE tries>0
|
||||
ORDER BY desired_timestamp, action DESC
|
||||
LIMIT 1;
|
||||
"#;
|
||||
params = paramsv![];
|
||||
params = vec![];
|
||||
};
|
||||
|
||||
loop {
|
||||
let job_res = context
|
||||
.sql
|
||||
.query_row_optional(query, params.clone(), |row| {
|
||||
.query_row_optional(query, rusqlite::params_from_iter(params.clone()), |row| {
|
||||
let job = Job {
|
||||
job_id: row.get("id")?,
|
||||
action: row.get("action")?,
|
||||
@@ -312,19 +313,21 @@ LIMIT 1;
|
||||
Ok(job) => return Ok(job),
|
||||
Err(err) => {
|
||||
// Remove invalid job from the DB
|
||||
info!(context, "cleaning up job, because of {:#}", err);
|
||||
info!(context, "Cleaning up job, because of {err:#}.");
|
||||
|
||||
// TODO: improve by only doing a single query
|
||||
let id = context
|
||||
.sql
|
||||
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
|
||||
.query_row(query, rusqlite::params_from_iter(params.clone()), |row| {
|
||||
row.get::<_, i32>(0)
|
||||
})
|
||||
.await
|
||||
.context("Failed to retrieve invalid job ID from the database")?;
|
||||
.context("failed to retrieve invalid job ID from the database")?;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
|
||||
.execute("DELETE FROM jobs WHERE id=?;", (id,))
|
||||
.await
|
||||
.with_context(|| format!("Failed to delete invalid job {id}"))?;
|
||||
.with_context(|| format!("failed to delete invalid job {id}"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,7 +346,7 @@ mod tests {
|
||||
"INSERT INTO jobs
|
||||
(added_timestamp, action, foreign_id, desired_timestamp)
|
||||
VALUES (?, ?, ?, ?);",
|
||||
paramsv![
|
||||
(
|
||||
now,
|
||||
if valid {
|
||||
Action::DownloadMsg as i32
|
||||
@@ -351,8 +354,8 @@ mod tests {
|
||||
-1
|
||||
},
|
||||
foreign_id,
|
||||
now
|
||||
],
|
||||
now,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user