mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 15:42:10 +03:00
Compare commits
70 Commits
v1.130.0
...
hpk/fix-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb61a6cf3b | ||
|
|
30f8522626 | ||
|
|
d3c221e061 | ||
|
|
8a421224f8 | ||
|
|
7dfce71ac9 | ||
|
|
35ba97f76a | ||
|
|
41921eaf3d | ||
|
|
03221ea86c | ||
|
|
b50761e4d1 | ||
|
|
40dea771cc | ||
|
|
e011f8f42f | ||
|
|
09d4b4354a | ||
|
|
ab151654fb | ||
|
|
ea9556b1b9 | ||
|
|
3dc6fd5c10 | ||
|
|
f39acbc037 | ||
|
|
005f7ff07e | ||
|
|
144ca7c171 | ||
|
|
7012b99d73 | ||
|
|
72bacd56f7 | ||
|
|
cc75038ccc | ||
|
|
f4810125e3 | ||
|
|
acf1faf151 | ||
|
|
255fbe94f7 | ||
|
|
b5d1eba28e | ||
|
|
1509978738 | ||
|
|
607b9e55a9 | ||
|
|
7c4c980409 | ||
|
|
b8ad3ec1b1 | ||
|
|
87dd33f66e | ||
|
|
7d8d13759a | ||
|
|
2b4f2a9171 | ||
|
|
8e869de350 | ||
|
|
b0ef082b2a | ||
|
|
bf8e74198d | ||
|
|
e77805471c | ||
|
|
45a8004b33 | ||
|
|
990f4dce9b | ||
|
|
0f36197c54 | ||
|
|
890a2bcc15 | ||
|
|
224355e83a | ||
|
|
c6ea4e389a | ||
|
|
678142b3fb | ||
|
|
ae6f83cd21 | ||
|
|
626b2be1fe | ||
|
|
ac5c789c75 | ||
|
|
ce2878f1e8 | ||
|
|
d4162899b4 | ||
|
|
e900d50e38 | ||
|
|
a438a4746a | ||
|
|
cfb819506f | ||
|
|
b86b915f40 | ||
|
|
ad5a5ad3db | ||
|
|
74081d8a36 | ||
|
|
34a434f07c | ||
|
|
dc944d8ca7 | ||
|
|
b06a7e7197 | ||
|
|
fa61d90115 | ||
|
|
7977c9ab44 | ||
|
|
6273a7d54e | ||
|
|
4d1a9c2aa1 | ||
|
|
b26ded423b | ||
|
|
e4b6eba5d7 | ||
|
|
bc225024a1 | ||
|
|
e616ecf160 | ||
|
|
f93562c6bf | ||
|
|
ac39c3699b | ||
|
|
091bc1ab13 | ||
|
|
fcbb66a788 | ||
|
|
ab2bc3bfb2 |
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.73.0
|
||||
RUSTUP_TOOLCHAIN: 1.74.0
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install rustfmt and clippy
|
||||
@@ -76,11 +76,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.73.0
|
||||
rust: 1.74.0
|
||||
- os: windows-latest
|
||||
rust: 1.73.0
|
||||
rust: 1.74.0
|
||||
- os: macos-latest
|
||||
rust: 1.73.0
|
||||
rust: 1.74.0
|
||||
|
||||
# Minimum Supported Rust Version = 1.70.0
|
||||
- os: ubuntu-latest
|
||||
|
||||
25
.github/workflows/upload-python-docs.yml
vendored
Normal file
25
.github/workflows/upload-python-docs.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Build & Deploy Documentation on py.delta.chat
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- name: Build Python documentation
|
||||
run: scripts/build-python-docs.sh
|
||||
- name: Upload to py.delta.chat
|
||||
uses: up9cloud/action-rsync@v1.3
|
||||
env:
|
||||
USER: delta
|
||||
KEY: ${{ secrets.CODESPEAK_KEY }}
|
||||
HOST: "lists.codespeak.net"
|
||||
SOURCE: "dist/html/"
|
||||
TARGET: "/home/delta/build/master"
|
||||
96
CHANGELOG.md
96
CHANGELOG.md
@@ -1,5 +1,95 @@
|
||||
# Changelog
|
||||
|
||||
## [1.131.5] - 2023-11-20
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-rpc-client: Add `Message.get_sender_contact()`.
|
||||
- Turn `ContactAddress` into an owned type.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Lowercase addresses in Autocrypt and Autocrypt-Gossip headers.
|
||||
- Lowercase the address in member added/removed messages.
|
||||
- Lowercase `addr` when it is set.
|
||||
- Do not replace the message with an error in square brackets when the sender is not a member of the protected group.
|
||||
|
||||
### Fixes
|
||||
|
||||
- `Chat::sync_contacts()`: Fetch contact addresses in a single query.
|
||||
- `Chat::rename_ex()`: Sync improved chat name to other devices.
|
||||
- Recognize `Chat-Group-Member-Added` of self case-insensitively.
|
||||
- Compare verifier addr to peerstate addr case-insensitively.
|
||||
|
||||
### Tests
|
||||
|
||||
- Port [Secure-Join](https://securejoin.readthedocs.io/) tests to JSON-RPC.
|
||||
|
||||
### CI
|
||||
|
||||
- Test with Rust 1.74.
|
||||
|
||||
|
||||
## [1.131.4] - 2023-11-16
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document DC_DOWNLOAD_UNDECIPHERABLE.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Always add "Member added" as system message.
|
||||
|
||||
## [1.131.3] - 2023-11-15
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update async-imap to 0.9.4 which does not ignore EOF on FETCH.
|
||||
- Reset gossiped timestamp on securejoin.
|
||||
- sync: Ignore unknown sync items to provide forward compatibility and avoid creating empty message bubbles.
|
||||
- sync: Skip sync when chat name is set to the current one.
|
||||
- Return connectivity HTML with an error when IO is stopped.
|
||||
|
||||
## [1.131.2] - 2023-11-14
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-rpc-client: add `Account.get_chat_by_contact()`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Do not post "... verified" messages on QR scan success.
|
||||
- Never drop better message from `apply_group_changes()`.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Assign MDNs to the trash chat early to prevent received MDNs from creating or unblocking 1:1 chats.
|
||||
- Allow to securejoin groups when 1:1 chat with the inviter is a contact request.
|
||||
- Add "setup changed" message for verified key before the message.
|
||||
- Ignore special chats when calculating similar chats.
|
||||
|
||||
## [1.131.1] - 2023-11-13
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not skip actual message parts when group change messages are inserted.
|
||||
|
||||
## [1.131.0] - 2023-11-13
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Sync chat contacts across devices ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)).
|
||||
- Sync creating broadcast lists across devices ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)).
|
||||
- Sync Chat::name across devices ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)).
|
||||
- Multi-device broadcast lists ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Encode chat name in the `List-ID` header to avoid SMTPUTF8 errors.
|
||||
- Ignore errors from generating sync messages.
|
||||
- `Context::execute_sync_items`: Ignore all errors ([#4817](https://github.com/deltachat/deltachat-core-rust/pull/4817)).
|
||||
- Allow to send unverified securejoin messages to protected chats ([#4982](https://github.com/deltachat/deltachat-core-rust/pull/4982)).
|
||||
|
||||
## [1.130.0] - 2023-11-10
|
||||
|
||||
### API-Changes
|
||||
@@ -3156,3 +3246,9 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.129.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.128.0...v1.129.0
|
||||
[1.129.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.129.0...v1.129.1
|
||||
[1.130.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.129.1...v1.130.0
|
||||
[1.131.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.130.0...v1.131.0
|
||||
[1.131.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.0...v1.131.1
|
||||
[1.131.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.1...v1.131.2
|
||||
[1.131.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.2...v1.131.3
|
||||
[1.131.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.3...v1.131.4
|
||||
[1.131.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.4...v1.131.5
|
||||
|
||||
@@ -86,6 +86,11 @@ For example:
|
||||
.with_context(|| format!("Unable to trash message {msg_id}"))
|
||||
```
|
||||
|
||||
All errors should be handled in one of these ways:
|
||||
- With `if let Err() =` (incl. logging them into `warn!()`/`err!()`).
|
||||
- With `.log_err().ok()`.
|
||||
- Bubbled up with `?`.
|
||||
|
||||
### Logging
|
||||
|
||||
For logging, use `info!`, `warn!` and `error!` macros.
|
||||
|
||||
251
Cargo.lock
generated
251
Cargo.lock
generated
@@ -203,7 +203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d37875bd9915b7d67c2f117ea2c30a0989874d0b2cb694fe25403c85763c0c9e"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener 3.0.1",
|
||||
"event-listener 3.1.0",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
@@ -224,9 +224,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.9.3"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e542b1682eba6b85a721daef0c58e79319ffd0c678565c07ac57c8071c548b5"
|
||||
checksum = "d736a74edf6c327b53dd9c932eae834253470ac5f0c55770e7e133bcbf986362"
|
||||
dependencies = [
|
||||
"async-channel 2.1.0",
|
||||
"base64 0.21.5",
|
||||
@@ -522,9 +522,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019"
|
||||
checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -585,9 +585,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cargo-platform"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12024c4645c97566567129c204f65d5815a8c9aecf30fcbe682b2fe034996d36"
|
||||
checksum = "e34637b3140142bdf929fb439e8aa4ebad7651ebf7b1080b3930aa16ac1459ff"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -707,18 +707,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.4.7"
|
||||
version = "4.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b"
|
||||
checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.4.7"
|
||||
version = "4.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663"
|
||||
checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
@@ -931,9 +931,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.5.3"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124"
|
||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
@@ -1087,7 +1087,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.130.0"
|
||||
version = "1.131.5"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1165,7 +1165,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.130.0"
|
||||
version = "1.131.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.1.0",
|
||||
@@ -1189,7 +1189,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.130.0"
|
||||
version = "1.131.5"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1204,7 +1204,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.130.0"
|
||||
version = "1.131.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1229,7 +1229,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.130.0"
|
||||
version = "1.131.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1479,15 +1479,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.16.8"
|
||||
version = "0.16.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4"
|
||||
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||
dependencies = [
|
||||
"der 0.7.8",
|
||||
"digest 0.10.7",
|
||||
"elliptic-curve 0.13.6",
|
||||
"elliptic-curve 0.13.8",
|
||||
"rfc6979 0.4.0",
|
||||
"signature 2.1.0",
|
||||
"signature 2.2.0",
|
||||
"spki 0.7.2",
|
||||
]
|
||||
|
||||
@@ -1508,7 +1508,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8 0.10.2",
|
||||
"signature 2.1.0",
|
||||
"signature 2.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1528,14 +1528,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
|
||||
checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0"
|
||||
dependencies = [
|
||||
"curve25519-dalek 4.1.1",
|
||||
"ed25519 2.2.3",
|
||||
"serde",
|
||||
"sha2 0.10.8",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -1578,12 +1579,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.6"
|
||||
version = "0.13.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914"
|
||||
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||
dependencies = [
|
||||
"base16ct 0.2.0",
|
||||
"crypto-bigint 0.5.3",
|
||||
"crypto-bigint 0.5.5",
|
||||
"digest 0.10.7",
|
||||
"ff 0.13.0",
|
||||
"generic-array",
|
||||
@@ -1744,9 +1745,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
|
||||
checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece"
|
||||
dependencies = [
|
||||
"humantime",
|
||||
"is-terminal",
|
||||
@@ -1763,9 +1764,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.5"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860"
|
||||
checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
@@ -1798,9 +1799,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "3.0.1"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01cec0252c2afff729ee6f00e903d479fba81784c8e2bd77447673471fdfaea1"
|
||||
checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
@@ -1813,15 +1814,15 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d96b852f1345da36d551b9473fa1e2b1eb5c5195585c6c018118bc92a8d91160"
|
||||
dependencies = [
|
||||
"event-listener 3.0.1",
|
||||
"event-listener 3.1.0",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
@@ -1899,9 +1900,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.2"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a481586acf778f1b1455424c343f71124b048ffa5f4fc3f8f6ae9dc432dcb3c7"
|
||||
checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
@@ -2099,9 +2100,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.10"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
||||
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -2150,9 +2151,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.21"
|
||||
version = "0.3.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
|
||||
checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -2160,7 +2161,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -2173,12 +2174,6 @@ version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.2"
|
||||
@@ -2195,7 +2190,7 @@ version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.2",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2301,9 +2296,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.9"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
|
||||
checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -2472,16 +2467,6 @@ dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown 0.12.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.1.0"
|
||||
@@ -2489,7 +2474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.2",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2679,9 +2664,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.26.0"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
|
||||
checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"openssl-sys",
|
||||
@@ -2697,9 +2682,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.10"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
|
||||
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@@ -2834,7 +2819,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
|
||||
dependencies = [
|
||||
"getrandom 0.2.10",
|
||||
"getrandom 0.2.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3175,8 +3160,8 @@ version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
|
||||
dependencies = [
|
||||
"ecdsa 0.16.8",
|
||||
"elliptic-curve 0.13.6",
|
||||
"ecdsa 0.16.9",
|
||||
"elliptic-curve 0.13.8",
|
||||
"primeorder",
|
||||
"sha2 0.10.8",
|
||||
]
|
||||
@@ -3198,8 +3183,8 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209"
|
||||
dependencies = [
|
||||
"ecdsa 0.16.8",
|
||||
"elliptic-curve 0.13.6",
|
||||
"ecdsa 0.16.9",
|
||||
"elliptic-curve 0.13.8",
|
||||
"primeorder",
|
||||
"sha2 0.10.8",
|
||||
]
|
||||
@@ -3296,8 +3281,8 @@ dependencies = [
|
||||
"derive_builder",
|
||||
"des",
|
||||
"digest 0.10.7",
|
||||
"ed25519-dalek 2.0.0",
|
||||
"elliptic-curve 0.13.6",
|
||||
"ed25519-dalek 2.1.0",
|
||||
"elliptic-curve 0.13.8",
|
||||
"flate2",
|
||||
"generic-array",
|
||||
"hex",
|
||||
@@ -3316,7 +3301,7 @@ dependencies = [
|
||||
"sha1",
|
||||
"sha2 0.10.8",
|
||||
"sha3",
|
||||
"signature 2.1.0",
|
||||
"signature 2.2.0",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"twofish",
|
||||
@@ -3516,11 +3501,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "primeorder"
|
||||
version = "0.13.3"
|
||||
version = "0.13.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7dbe9ed3b56368bd99483eb32fe9c17fdd3730aebadc906918ce78d54c7eeb4"
|
||||
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
|
||||
dependencies = [
|
||||
"elliptic-curve 0.13.6",
|
||||
"elliptic-curve 0.13.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3558,9 +3543,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proptest"
|
||||
version = "1.3.1"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e"
|
||||
checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"lazy_static",
|
||||
@@ -3568,7 +3553,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_xorshift",
|
||||
"regex-syntax 0.7.5",
|
||||
"regex-syntax 0.8.2",
|
||||
"unarray",
|
||||
]
|
||||
|
||||
@@ -3631,9 +3616,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.10.5"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c78e758510582acc40acb90458401172d41f1016f8c9dde89e49677afb7eec1"
|
||||
checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"rand 0.8.5",
|
||||
@@ -3744,7 +3729,7 @@ version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.10",
|
||||
"getrandom 0.2.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3825,7 +3810,7 @@ version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4"
|
||||
dependencies = [
|
||||
"getrandom 0.2.10",
|
||||
"getrandom 0.2.11",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
@@ -3868,12 +3853,6 @@ version = "0.6.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.2"
|
||||
@@ -3971,7 +3950,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"getrandom 0.2.10",
|
||||
"getrandom 0.2.11",
|
||||
"libc",
|
||||
"spin 0.9.8",
|
||||
"untrusted 0.9.0",
|
||||
@@ -4022,7 +4001,7 @@ dependencies = [
|
||||
"pkcs1 0.7.5",
|
||||
"pkcs8 0.10.2",
|
||||
"rand_core 0.6.4",
|
||||
"signature 2.1.0",
|
||||
"signature 2.2.0",
|
||||
"spki 0.7.2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
@@ -4030,9 +4009,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.29.0"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
|
||||
checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"fallible-iterator",
|
||||
@@ -4080,9 +4059,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.21"
|
||||
version = "0.38.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
|
||||
checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"errno",
|
||||
@@ -4093,9 +4072,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.8"
|
||||
version = "0.21.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c"
|
||||
checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9"
|
||||
dependencies = [
|
||||
"ring 0.17.5",
|
||||
"rustls-webpki",
|
||||
@@ -4116,9 +4095,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64 0.21.5",
|
||||
]
|
||||
@@ -4204,9 +4183,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.15"
|
||||
version = "0.8.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c"
|
||||
checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"schemars_derive",
|
||||
@@ -4216,9 +4195,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.15"
|
||||
version = "0.8.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c"
|
||||
checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4295,9 +4274,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "self_cell"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6"
|
||||
checksum = "e388332cd64eb80cd595a00941baf513caffae8dce9cfd0467fc9c66397dade6"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
@@ -4310,9 +4289,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.191"
|
||||
version = "1.0.192"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a834c4821019838224821468552240d4d95d14e751986442c816572d39a080c9"
|
||||
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -4337,9 +4316,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.191"
|
||||
version = "1.0.192"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46fa52d5646bce91b680189fe5b1c049d2ea38dabb4e2e7c8d00ca12cfbfbcfd"
|
||||
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4485,9 +4464,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
"rand_core 0.6.4",
|
||||
@@ -4510,9 +4489,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.11.1"
|
||||
version = "1.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
|
||||
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
@@ -4749,9 +4728,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"
|
||||
checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
@@ -4878,9 +4857,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.33.0"
|
||||
version = "1.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
|
||||
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -4907,9 +4886,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
||||
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5020,7 +4999,7 @@ version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
|
||||
dependencies = [
|
||||
"indexmap 2.1.0",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
@@ -5100,9 +5079,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.1.4"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
@@ -5111,9 +5090,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
||||
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
@@ -5279,11 +5258,11 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.5.0"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
|
||||
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
|
||||
dependencies = [
|
||||
"getrandom 0.2.10",
|
||||
"getrandom 0.2.11",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -5700,18 +5679,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.25"
|
||||
version = "0.7.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557"
|
||||
checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.25"
|
||||
version = "0.7.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b"
|
||||
checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5720,9 +5699,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.6.0"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
|
||||
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.130.0"
|
||||
version = "1.131.5"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.70"
|
||||
@@ -72,7 +72,7 @@ quick-xml = "0.31"
|
||||
rand = "0.8"
|
||||
regex = "1.9"
|
||||
reqwest = { version = "0.11.20", features = ["json"] }
|
||||
rusqlite = { version = "0.29", features = ["sqlcipher"] }
|
||||
rusqlite = { version = "0.30", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
sanitize-filename = "0.5"
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.130.0"
|
||||
version = "1.131.5"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -2561,7 +2561,7 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
|
||||
* the Verified-Group-Invite protocol is offered in the QR code;
|
||||
* works for protected groups as well as for normal groups.
|
||||
* If set to 0, the Setup-Contact protocol is offered in the QR code.
|
||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
* See https://securejoin.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
* @return The text that should go to the QR code,
|
||||
* On errors, an empty QR code is returned, NULL is never returned.
|
||||
@@ -2597,7 +2597,7 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
|
||||
*
|
||||
* Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes.
|
||||
*
|
||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
* See https://securejoin.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -4580,15 +4580,18 @@ int dc_msg_has_html (dc_msg_t* msg);
|
||||
* if they are larger than the limit set by the dc_set_config()-option `download_limit`.
|
||||
*
|
||||
* The function returns one of:
|
||||
* - @ref DC_DOWNLOAD_DONE - The message does not need any further download action
|
||||
* and should be rendered as usual.
|
||||
* - @ref DC_DOWNLOAD_AVAILABLE - There is additional content to download.
|
||||
* In addition to the usual message rendering,
|
||||
* the UI shall show a download button that calls dc_download_full_msg()
|
||||
* - @ref DC_DOWNLOAD_IN_PROGRESS - Download was started with dc_download_full_msg() and is still in progress.
|
||||
* If the download fails or succeeds,
|
||||
* the event @ref DC_EVENT_MSGS_CHANGED is emitted.
|
||||
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_download_full_msg() again.
|
||||
* - @ref DC_DOWNLOAD_DONE - The message does not need any further download action
|
||||
* and should be rendered as usual.
|
||||
* - @ref DC_DOWNLOAD_AVAILABLE - There is additional content to download.
|
||||
* In addition to the usual message rendering,
|
||||
* the UI shall show a download button that calls dc_download_full_msg()
|
||||
* - @ref DC_DOWNLOAD_IN_PROGRESS - Download was started with dc_download_full_msg() and is still in progress.
|
||||
* If the download fails or succeeds,
|
||||
* the event @ref DC_EVENT_MSGS_CHANGED is emitted.
|
||||
*
|
||||
* - @ref DC_DOWNLOAD_UNDECIPHERABLE - The message does not need any futher download action.
|
||||
* It was fully downloaded, but we failed to decrypt it.
|
||||
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_download_full_msg() again.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
@@ -6433,22 +6436,27 @@ void dc_event_unref(dc_event_t* event);
|
||||
/**
|
||||
* Download not needed, see dc_msg_get_download_state() for details.
|
||||
*/
|
||||
#define DC_DOWNLOAD_DONE 0
|
||||
#define DC_DOWNLOAD_DONE 0
|
||||
|
||||
/**
|
||||
* Download available, see dc_msg_get_download_state() for details.
|
||||
*/
|
||||
#define DC_DOWNLOAD_AVAILABLE 10
|
||||
#define DC_DOWNLOAD_AVAILABLE 10
|
||||
|
||||
/**
|
||||
* Download failed, see dc_msg_get_download_state() for details.
|
||||
*/
|
||||
#define DC_DOWNLOAD_FAILURE 20
|
||||
#define DC_DOWNLOAD_FAILURE 20
|
||||
|
||||
/**
|
||||
* Download not needed, see dc_msg_get_download_state() for details.
|
||||
*/
|
||||
#define DC_DOWNLOAD_UNDECIPHERABLE 30
|
||||
|
||||
/**
|
||||
* Download in progress, see dc_msg_get_download_state() for details.
|
||||
*/
|
||||
#define DC_DOWNLOAD_IN_PROGRESS 1000
|
||||
#define DC_DOWNLOAD_IN_PROGRESS 1000
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.130.0"
|
||||
version = "1.131.5"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -678,7 +678,7 @@ impl CommandApi {
|
||||
/// the Verified-Group-Invite protocol is offered in the QR code;
|
||||
/// works for protected groups as well as for normal groups.
|
||||
/// If not set, the Setup-Contact protocol is offered in the QR code.
|
||||
/// See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
/// See https://securejoin.readthedocs.io/en/latest/new.html
|
||||
/// for details about both protocols.
|
||||
///
|
||||
/// return format: `[code, svg]`
|
||||
@@ -707,7 +707,7 @@ impl CommandApi {
|
||||
///
|
||||
/// Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||
///
|
||||
/// See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
/// See https://securejoin.readthedocs.io/en/latest/new.html
|
||||
/// for details about both protocols.
|
||||
///
|
||||
/// **qr**: The text of the scanned QR code. Typically, the same string as given
|
||||
|
||||
@@ -53,5 +53,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.130.0"
|
||||
"version": "1.131.5"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.130.0"
|
||||
version = "1.131.5"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
|
||||
@@ -11,7 +11,7 @@ deltachat = { path = "..", features = ["internals"]}
|
||||
dirs = "5"
|
||||
log = "0.4.20"
|
||||
pretty_env_logger = "0.5"
|
||||
rusqlite = "0.29"
|
||||
rusqlite = "0.30"
|
||||
rustyline = "12"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
|
||||
|
||||
0
deltachat-rpc-client/examples/echobot_advanced.py
Normal file → Executable file
0
deltachat-rpc-client/examples/echobot_advanced.py
Normal file → Executable file
8
deltachat-rpc-client/examples/echobot_no_hooks.py
Normal file → Executable file
8
deltachat-rpc-client/examples/echobot_no_hooks.py
Normal file → Executable file
@@ -40,13 +40,13 @@ def main():
|
||||
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event["type"] == EventType.INFO:
|
||||
if event["kind"] == EventType.INFO:
|
||||
logging.info("%s", event["msg"])
|
||||
elif event["type"] == EventType.WARNING:
|
||||
elif event["kind"] == EventType.WARNING:
|
||||
logging.warning("%s", event["msg"])
|
||||
elif event["type"] == EventType.ERROR:
|
||||
elif event["kind"] == EventType.ERROR:
|
||||
logging.error("%s", event["msg"])
|
||||
elif event["type"] == EventType.INCOMING_MSG:
|
||||
elif event["kind"] == EventType.INCOMING_MSG:
|
||||
logging.info("Got an incoming message")
|
||||
process_messages()
|
||||
|
||||
|
||||
@@ -111,6 +111,20 @@ class Account:
|
||||
contacts = self._rpc.get_blocked_contacts(self.id)
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
|
||||
def get_chat_by_contact(self, contact: Union[int, Contact]) -> Optional[Chat]:
|
||||
"""Return 1:1 chat for a contact if it exists."""
|
||||
if isinstance(contact, Contact):
|
||||
assert contact.account == self
|
||||
contact_id = contact.id
|
||||
elif isinstance(contact, int):
|
||||
contact_id = contact
|
||||
else:
|
||||
raise ValueError(f"{contact!r} is not a contact")
|
||||
chat_id = self._rpc.get_chat_id_by_contact_id(self.id, contact_id)
|
||||
if chat_id:
|
||||
return Chat(self, chat_id)
|
||||
return None
|
||||
|
||||
def get_contacts(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
@@ -204,7 +218,7 @@ class Account:
|
||||
The function returns immediately and the handshake runs in background, sending
|
||||
and receiving several messages.
|
||||
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||
See https://countermitm.readthedocs.io/en/latest/new.html for protocol details.
|
||||
See https://securejoin.readthedocs.io/en/latest/new.html for protocol details.
|
||||
|
||||
:param qrdata: The text of the scanned QR code.
|
||||
"""
|
||||
@@ -257,6 +271,18 @@ class Account:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
return event
|
||||
|
||||
def wait_for_securejoin_inviter_success(self):
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
def wait_for_securejoin_joiner_success(self):
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
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(
|
||||
|
||||
@@ -195,7 +195,7 @@ class Client:
|
||||
|
||||
|
||||
class Bot(Client):
|
||||
"""Simple bot implementation that listent to events of a single account."""
|
||||
"""Simple bot implementation that listens to events of a single account."""
|
||||
|
||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
kwargs.setdefault("bot", "1")
|
||||
|
||||
@@ -42,6 +42,10 @@ class Message:
|
||||
return AttrDict(reactions)
|
||||
return None
|
||||
|
||||
def get_sender_contact(self) -> Contact:
|
||||
from_id = self.get_snapshot().from_id
|
||||
return self.account.get_contact_by_id(from_id)
|
||||
|
||||
def mark_seen(self) -> None:
|
||||
"""Mark the message as seen."""
|
||||
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
|
||||
@@ -9,20 +9,14 @@ def test_qr_setup_contact(acfactory) -> None:
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
||||
@@ -35,24 +29,31 @@ def test_qr_securejoin(acfactory):
|
||||
|
||||
logging.info("Alice creates a verified group")
|
||||
alice_chat = alice.create_group("Verified group", protect=True)
|
||||
assert alice_chat.get_basic_snapshot().is_protected
|
||||
|
||||
logging.info("Bob joins verified group")
|
||||
qr_code, _svg = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# Check that at least some of the handshake messages are deleted.
|
||||
for ac in [alice, bob]:
|
||||
while True:
|
||||
event = ac.wait_for_event()
|
||||
if event["kind"] == "ImapMessageDeleted":
|
||||
break
|
||||
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
|
||||
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
||||
@@ -60,7 +61,97 @@ def test_qr_securejoin(acfactory):
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
|
||||
def test_verified_group_recovery(acfactory, rpc) -> None:
|
||||
def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
bob_chat_alice = snapshot.chat
|
||||
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
||||
|
||||
alice_chat = alice.create_group("Verified group", protect=True)
|
||||
logging.info("Bob joins verified group")
|
||||
qr_code, _svg = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# Chat stays being a contact request.
|
||||
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
||||
|
||||
|
||||
def test_qr_readreceipt(acfactory) -> None:
|
||||
alice, bob, charlie = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("Bob and Charlie setup contact with Alice")
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
|
||||
bob.secure_join(qr_code)
|
||||
charlie.secure_join(qr_code)
|
||||
|
||||
for joiner in [bob, charlie]:
|
||||
joiner.wait_for_securejoin_joiner_success()
|
||||
|
||||
logging.info("Alice creates a verified group")
|
||||
group = alice.create_group("Group", protect=True)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
charlie_addr = charlie.get_config("addr")
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_charlie = alice.create_contact(charlie_addr, "Charlie")
|
||||
|
||||
group.add_contact(alice_contact_bob)
|
||||
group.add_contact(alice_contact_charlie)
|
||||
|
||||
# Promote a group.
|
||||
group.send_message(text="Hello")
|
||||
|
||||
logging.info("Bob and Charlie receive a group")
|
||||
|
||||
bob_msg_id = bob.wait_for_incoming_msg_event().msg_id
|
||||
bob_message = bob.get_message_by_id(bob_msg_id)
|
||||
bob_snapshot = bob_message.get_snapshot()
|
||||
assert bob_snapshot.text == "Hello"
|
||||
|
||||
# Charlie receives the same "Hello" message as Bob.
|
||||
charlie.wait_for_incoming_msg_event()
|
||||
|
||||
logging.info("Bob sends a message to the group")
|
||||
|
||||
bob_out_message = bob_snapshot.chat.send_message(text="Hi from Bob!")
|
||||
|
||||
charlie_msg_id = charlie.wait_for_incoming_msg_event().msg_id
|
||||
charlie_message = charlie.get_message_by_id(charlie_msg_id)
|
||||
charlie_snapshot = charlie_message.get_snapshot()
|
||||
assert charlie_snapshot.text == "Hi from Bob!"
|
||||
|
||||
bob_contact_charlie = bob.create_contact(charlie_addr, "Charlie")
|
||||
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||
|
||||
logging.info("Charlie reads Bob's message")
|
||||
charlie_message.mark_seen()
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event["kind"] == "MsgRead" and event["msg_id"] == bob_out_message.id:
|
||||
break
|
||||
|
||||
# Receiving a read receipt from Charlie
|
||||
# should not unblock hidden chat with Charlie for Bob.
|
||||
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||
|
||||
|
||||
def test_verified_group_recovery(acfactory) -> None:
|
||||
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("ac1 creates verified group")
|
||||
@@ -70,10 +161,7 @@ def test_verified_group_recovery(acfactory, rpc) -> None:
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
# ac1 has ac2 directly verified.
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
@@ -81,10 +169,7 @@ def test_verified_group_recovery(acfactory, rpc) -> None:
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
ac3_chat = ac3.secure_join(qr_code)
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
@@ -93,69 +178,44 @@ def test_verified_group_recovery(acfactory, rpc) -> None:
|
||||
qr_code, _svg = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
|
||||
while True:
|
||||
event = ac3.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
ac3.wait_for_securejoin_inviter_success()
|
||||
|
||||
logging.info("ac3 sends a message to the group")
|
||||
assert len(ac3_chat.get_contacts()) == 3
|
||||
ac3_chat.send_text("Hi!")
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
logging.info("Received message %s", snapshot.text)
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Hi!"
|
||||
|
||||
# ac1 contact cannot be verified by ac2 because ac3 did not gossip ac1 key in the "Hi!" message.
|
||||
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
|
||||
assert not ac1_contact.get_snapshot().is_verified
|
||||
|
||||
ac3_contact_id_ac1 = rpc.lookup_contact_id_by_addr(ac3.id, ac1.get_config("addr"))
|
||||
ac3_chat.remove_contact(ac3_contact_id_ac1)
|
||||
ac3_chat.add_contact(ac3_contact_id_ac1)
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
logging.info("ac2 got event message: %s", snapshot.text)
|
||||
assert "removed" in snapshot.text
|
||||
|
||||
event = ac2.wait_for_incoming_msg_event()
|
||||
msg_id = event.msg_id
|
||||
chat_id = event.chat_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
logging.info("ac2 got event message: %s", snapshot.text)
|
||||
assert "added" in snapshot.text
|
||||
assert snapshot.text == "Hi!"
|
||||
|
||||
# ac1 contact is verified for ac2 because ac3 gossiped ac1 key in the "Hi!" message.
|
||||
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
|
||||
assert ac1_contact.get_snapshot().is_verified
|
||||
|
||||
chat = Chat(ac2, chat_id)
|
||||
chat.send_text("Works again!")
|
||||
# ac2 can write messages to the group.
|
||||
snapshot.chat.send_text("Works again!")
|
||||
|
||||
msg_id = ac3.wait_for_incoming_msg_event().msg_id
|
||||
message = ac3.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
snapshot = ac3.get_message_by_id(ac3.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
ac1.wait_for_incoming_msg_event() # Hi!
|
||||
ac1.wait_for_incoming_msg_event() # Member removed
|
||||
ac1.wait_for_incoming_msg_event() # Member added
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
ac1_chat_messages = snapshot.chat.get_messages()
|
||||
ac2_addr = ac2.get_config("addr")
|
||||
assert ac1_chat_messages[-2].get_snapshot().text == f"Changed setup for {ac2_addr}"
|
||||
|
||||
# ac2 is now verified by ac3 for ac1
|
||||
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
|
||||
|
||||
ac1_chat_messages = snapshot.chat.get_messages()
|
||||
ac2_addr = ac2.get_config("addr")
|
||||
assert ac1_chat_messages[-1].get_snapshot().text == f"Changed setup for {ac2_addr}"
|
||||
|
||||
|
||||
def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
"""Tests verified group recovery by reverifiying than removing and adding a member back."""
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("ac1 creates verified group")
|
||||
@@ -165,10 +225,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
# ac1 has ac2 directly verified.
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
@@ -176,10 +233,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
ac3_chat = ac3.secure_join(qr_code)
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
@@ -188,10 +242,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
qr_code, _svg = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
|
||||
while True:
|
||||
event = ac3.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
ac3.wait_for_securejoin_inviter_success()
|
||||
|
||||
logging.info("ac3 sends a message to the group")
|
||||
assert len(ac3_chat.get_contacts()) == 3
|
||||
@@ -247,3 +298,134 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
# ac2 is now verified by ac3 for ac1
|
||||
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
|
||||
|
||||
|
||||
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
"""Regression test for
|
||||
issue <https://github.com/deltachat/deltachat-core-rust/issues/4894>.
|
||||
"""
|
||||
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
||||
|
||||
logging.info("ac3: verify with ac2")
|
||||
qr_code, _svg = ac2.get_qr_code()
|
||||
ac3.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_inviter_success()
|
||||
|
||||
# in order for ac2 to have pending bobstate with a verified group
|
||||
# we first create a fully joined verified group, and then start
|
||||
# joining a second time but interrupt it, to create pending bob state
|
||||
|
||||
logging.info("ac1: create verified group that ac2 fully joins")
|
||||
ch1 = ac1.create_group("Group", protect=True)
|
||||
qr_code, _svg = ch1.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
# ensure ac1 can write and ac2 receives messages in verified chat
|
||||
ch1.send_text("ac1 says hello")
|
||||
while 1:
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
if snapshot.text == "ac1 says hello":
|
||||
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||
break
|
||||
|
||||
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
|
||||
qr_code, _svg = ch1.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.remove()
|
||||
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
|
||||
|
||||
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
|
||||
assert ac3.get_contact_by_addr(ac2.get_config("addr")).get_snapshot().is_verified
|
||||
assert ac2.get_contact_by_addr(ac3.get_config("addr")).get_snapshot().is_verified
|
||||
|
||||
logging.info("ac3: create a verified group VG with ac2")
|
||||
vg = ac3.create_group("ac3-created", protect=True)
|
||||
vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr")))
|
||||
|
||||
# ensure ac2 receives message in VG
|
||||
vg.send_text("hello")
|
||||
while 1:
|
||||
msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
if msg.text == "hello":
|
||||
assert msg.chat.get_basic_snapshot().is_protected
|
||||
break
|
||||
|
||||
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
|
||||
qr_code, _svg = vg.get_qr_code()
|
||||
ac4.secure_join(qr_code)
|
||||
ac3.wait_for_securejoin_inviter_success()
|
||||
while 1:
|
||||
ev = ac2.wait_for_event()
|
||||
if "added by unrelated SecureJoin" in str(ev):
|
||||
return
|
||||
|
||||
|
||||
def test_qr_new_group_unblocked(acfactory):
|
||||
"""Regression test for a bug introduced in core v1.113.0.
|
||||
ac2 scans a verified group QR code created by ac1.
|
||||
This results in creation of a blocked 1:1 chat with ac1 on ac2,
|
||||
but ac1 contact is not blocked on ac2.
|
||||
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
|
||||
ac2 should receive a message and create a contact request for the group.
|
||||
Due to a bug previously ac2 created a blocked group.
|
||||
"""
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_chat = ac1.create_group("Group for joining", protect=True)
|
||||
qr_code, _svg = ac1_chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
ac1_new_chat = ac1.create_group("Another group")
|
||||
ac1_new_chat.add_contact(ac1.get_contact_by_addr(ac2.get_config("addr")))
|
||||
# Receive "Member added" message.
|
||||
ac2.wait_for_incoming_msg_event()
|
||||
|
||||
ac1_new_chat.send_text("Hello!")
|
||||
ac2_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert ac2_msg.text == "Hello!"
|
||||
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
|
||||
|
||||
|
||||
def test_aeap_flow_verified(acfactory):
|
||||
"""Test that a new address is added to a contact when it changes its address."""
|
||||
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group("hello", protect=True)
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
logging.info("ac2: start QR-code based join-group protocol")
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
logging.info("sending first message")
|
||||
msg_out = chat.send_text("old address").get_snapshot()
|
||||
|
||||
logging.info("receiving first message")
|
||||
ac2.wait_for_incoming_msg_event() # member added message
|
||||
msg_in_1 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert msg_in_1.text == msg_out.text
|
||||
|
||||
logging.info("changing email account")
|
||||
ac1.set_config("addr", ac1new.get_config("addr"))
|
||||
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
|
||||
ac1.stop_io()
|
||||
ac1.configure()
|
||||
ac1.start_io()
|
||||
|
||||
logging.info("sending second message")
|
||||
msg_out = chat.send_text("changed address").get_snapshot()
|
||||
|
||||
logging.info("receiving second message")
|
||||
msg_in_2 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
|
||||
msg_in_2_snapshot = msg_in_2.get_snapshot()
|
||||
assert msg_in_2_snapshot.text == msg_out.text
|
||||
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
|
||||
assert msg_in_2.get_sender_contact().get_snapshot().address == ac1new.get_config("addr")
|
||||
assert len(msg_in_2_snapshot.chat.get_contacts()) == 2
|
||||
assert ac1new.get_config("addr") in [
|
||||
contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.130.0"
|
||||
version = "1.131.5"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -27,8 +27,6 @@ skip = [
|
||||
{ name = "ed25519", version = "1.5.3" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "hashbrown", version = "<0.14.0" },
|
||||
{ name = "indexmap", version = "<2.0.0" },
|
||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||
{ name = "pkcs8", version = "0.9.0" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
|
||||
@@ -28,6 +28,7 @@ module.exports = {
|
||||
DC_DOWNLOAD_DONE: 0,
|
||||
DC_DOWNLOAD_FAILURE: 20,
|
||||
DC_DOWNLOAD_IN_PROGRESS: 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE: 30,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
|
||||
DC_EVENT_CHAT_MODIFIED: 2020,
|
||||
DC_EVENT_CONFIGURE_PROGRESS: 2041,
|
||||
|
||||
@@ -28,6 +28,7 @@ export enum C {
|
||||
DC_DOWNLOAD_DONE = 0,
|
||||
DC_DOWNLOAD_FAILURE = 20,
|
||||
DC_DOWNLOAD_IN_PROGRESS = 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE = 30,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
|
||||
DC_EVENT_CHAT_MODIFIED = 2020,
|
||||
DC_EVENT_CONFIGURE_PROGRESS = 2041,
|
||||
|
||||
@@ -56,5 +56,5 @@
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.130.0"
|
||||
"version": "1.131.5"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
=========================
|
||||
DeltaChat Python bindings
|
||||
=========================
|
||||
============================
|
||||
CFFI Python Bindings
|
||||
============================
|
||||
|
||||
This package provides `Python bindings`_ to the `deltachat-core library`_
|
||||
which implements IMAP/SMTP/MIME/OpenPGP e-mail standards and offers
|
||||
@@ -8,157 +8,3 @@ a low-level Chat/Contact/Message API to user interfaces and bots.
|
||||
|
||||
.. _`deltachat-core library`: https://github.com/deltachat/deltachat-core-rust
|
||||
.. _`Python bindings`: https://py.delta.chat/
|
||||
|
||||
Installing pre-built packages (Linux-only)
|
||||
==========================================
|
||||
|
||||
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 create a fresh Python virtual environment
|
||||
and activate it in your shell::
|
||||
|
||||
python -m venv env
|
||||
source env/bin/activate
|
||||
|
||||
Afterwards, invoking ``python`` or ``pip install`` only
|
||||
modifies files in your ``env`` directory and leaves
|
||||
your system installation alone.
|
||||
|
||||
For Linux we build wheels for all releases and push them to a python package
|
||||
index. To install the latest release::
|
||||
|
||||
pip install deltachat
|
||||
|
||||
To verify it worked::
|
||||
|
||||
python -c "import deltachat"
|
||||
|
||||
Running tests
|
||||
=============
|
||||
|
||||
Recommended way to run tests is using `scripts/run-python-test.sh`
|
||||
script provided in the core repository.
|
||||
|
||||
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
|
||||
--------------------------------------------
|
||||
|
||||
If you want to run live functional tests
|
||||
you can set ``CHATMAIL_DOMAIN`` to a domain of the email server
|
||||
that creates e-mail accounts like this::
|
||||
|
||||
export CHATMAIL_DOMAIN=nine.testrun.org
|
||||
|
||||
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the server.
|
||||
These accounts have the pattern `ci-{6 characters}@{CHATMAIL_DOMAIN}`.
|
||||
After setting the variable, either rerun `scripts/run-python-test.sh`
|
||||
or run offline and online tests with `tox` directly::
|
||||
|
||||
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`::
|
||||
|
||||
export DCC_RS_DEV="$PWD"
|
||||
export DCC_RS_TARGET=debug
|
||||
tox -c python --devenv env -e py
|
||||
. 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
|
||||
===============================
|
||||
|
||||
Install Rust and Cargo first.
|
||||
The easiest is probably to use `rustup <https://rustup.rs/>`_.
|
||||
|
||||
Bootstrap Rust and Cargo by using rustup::
|
||||
|
||||
curl https://sh.rustup.rs -sSf | sh
|
||||
|
||||
Then clone the deltachat-core-rust repo::
|
||||
|
||||
git clone https://github.com/deltachat/deltachat-core-rust
|
||||
cd deltachat-core-rust
|
||||
|
||||
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.
|
||||
|
||||
First, build the core library::
|
||||
|
||||
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
|
||||
|
||||
Build and install the bindings:
|
||||
|
||||
export DCC_RS_DEV="$PWD"
|
||||
export DCC_RS_TARGET=release
|
||||
python -m pip install ./python
|
||||
|
||||
`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
|
||||
===============================
|
||||
|
||||
Building portable manylinux wheels which come with libdeltachat.so
|
||||
can be done with Docker_ or Podman_.
|
||||
|
||||
.. _Docker: https://www.docker.com/
|
||||
.. _Podman: https://podman.io/
|
||||
|
||||
If you want to build your own wheels, build container image first::
|
||||
|
||||
$ cd deltachat-core-rust # cd to deltachat-core-rust working tree
|
||||
$ docker build -t deltachat/coredeps scripts/coredeps
|
||||
|
||||
This will use the ``scripts/coredeps/Dockerfile`` to build
|
||||
container image called ``deltachat/coredeps``. You can afterwards
|
||||
find it with::
|
||||
|
||||
$ docker images
|
||||
|
||||
This docker image can be used to run tests and build Python wheels for all interpreters::
|
||||
|
||||
$ docker run -e CHATMAIL_DOMAIN \
|
||||
--rm -it -v $(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps scripts/run_all.sh
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
VERSION = $(shell python -c "import conf ; print(conf.version)")
|
||||
DOCZIP = devpi-$(VERSION).doc.zip
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
RSYNCOPTS = -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||
|
||||
export HOME=/tmp/home
|
||||
export TESTHOME=$(HOME)
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
# This variable is not auto generated as the order is important.
|
||||
USER_MAN_CHAPTERS = commands\
|
||||
user\
|
||||
indices\
|
||||
packages\
|
||||
# userman/index.rst\
|
||||
# userman/devpi_misc.rst\
|
||||
# userman/devpi_concepts.rst\
|
||||
|
||||
|
||||
#export DEVPI_CLIENTDIR=$(CURDIR)/.tmp_devpi_user_man/client
|
||||
#export DEVPI_SERVERDIR=$(CURDIR)/.tmp_devpi_user_man/server
|
||||
|
||||
chapter = commands
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp \
|
||||
epub latex latexpdf text man changes linkcheck doctest gettext install \
|
||||
quickstart-releaseprocess quickstart-pypimirror quickstart-server regen \
|
||||
prepare-quickstart\
|
||||
regen.server-fresh regen.server-restart regen.server-clean\
|
||||
regen.uman-all regen.uman
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo
|
||||
@echo "User Manual Regen Targets"
|
||||
@echo " regen.uman regenerates page. of the user manual chapeter e.g. regen.uman chapter=..."
|
||||
@echo " regen.uman-all regenerates the user manual"
|
||||
@echo " regen.uman-clean stop temp server and clean up directory"
|
||||
@echo " Chapter List: $(USER_MAN_CHAPTERS)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
version:
|
||||
@echo "version $(VERSION)"
|
||||
|
||||
doczip: html
|
||||
python doczip.py $(DOCZIP) _build/html
|
||||
|
||||
install: html
|
||||
rsync -avz $(RSYNCOPTS) _build/html/ delta@py.delta.chat:build/master
|
||||
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/devpi.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/devpi.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/devpi"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/devpi"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
|
||||
17
python/doc/_templates/globaltoc.html
vendored
17
python/doc/_templates/globaltoc.html
vendored
@@ -1,17 +0,0 @@
|
||||
|
||||
<div class="globaltoc">
|
||||
|
||||
<ul>
|
||||
<li><a href="{{ pathto('index') }}">index</a></li>
|
||||
<li><a href="{{ pathto('install') }}">install</a></li>
|
||||
<li><a href="{{ pathto('api') }}">high level API</a></li>
|
||||
<li><a href="{{ pathto('lapi') }}">low level API</a></li>
|
||||
</ul>
|
||||
<b>external links:</b>
|
||||
<ul>
|
||||
<li><a href="https://github.com/deltachat/deltachat-core-rust">github repository</a></li>
|
||||
<li><a href="https://pypi.python.org/pypi/deltachat">pypi: deltachat</a></li>
|
||||
<li><a href="https://web.libera.chat/#deltachat">#deltachat</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
1
python/doc/_templates/sidebarintro.html
vendored
1
python/doc/_templates/sidebarintro.html
vendored
@@ -1 +0,0 @@
|
||||
<h3>deltachat {{release}}</h3>
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
high level API reference
|
||||
High Level API Reference
|
||||
========================
|
||||
|
||||
- :class:`deltachat.Account` (your main entry point, creates the
|
||||
@@ -8,28 +7,14 @@ high level API reference
|
||||
- :class:`deltachat.Chat`
|
||||
- :class:`deltachat.Message`
|
||||
|
||||
Account
|
||||
-------
|
||||
|
||||
.. autoclass:: deltachat.Account
|
||||
:members:
|
||||
|
||||
|
||||
Contact
|
||||
-------
|
||||
:members:
|
||||
|
||||
.. autoclass:: deltachat.Contact
|
||||
:members:
|
||||
|
||||
Chat
|
||||
----
|
||||
:members:
|
||||
|
||||
.. autoclass:: deltachat.Chat
|
||||
:members:
|
||||
|
||||
Message
|
||||
-------
|
||||
:members:
|
||||
|
||||
.. autoclass:: deltachat.Message
|
||||
:members:
|
||||
|
||||
:members:
|
||||
@@ -1,11 +1,10 @@
|
||||
|
||||
examples
|
||||
Examples
|
||||
========
|
||||
|
||||
Once you have :doc:`installed deltachat bindings <install>`
|
||||
you need email/password credentials for an IMAP/SMTP account.
|
||||
Delta Chat developers and the CI system use a special URL to create
|
||||
temporary e-mail accounts on [testrun.org](https://testrun.org) for testing.
|
||||
temporary email accounts on `testrun.org <https://testrun.org/>`_ for testing.
|
||||
|
||||
Receiving a Chat message from the command line
|
||||
----------------------------------------------
|
||||
@@ -16,11 +15,11 @@ Here is a simple bot that:
|
||||
|
||||
- terminates the bot if the message `/quit` is sent
|
||||
|
||||
.. include:: ../examples/echo_and_quit.py
|
||||
.. include:: ../../examples/echo_and_quit.py
|
||||
:literal:
|
||||
|
||||
With this file in your working directory you can run the bot
|
||||
by specifying a database path, an e-mail address and password of
|
||||
by specifying a database path, an email address and password of
|
||||
a SMTP-IMAP account::
|
||||
|
||||
$ cd examples
|
||||
@@ -40,11 +39,11 @@ Here is a simple bot that:
|
||||
|
||||
- tracks member additions and removals for all chat groups
|
||||
|
||||
.. include:: ../examples/group_tracking.py
|
||||
.. include:: ../../examples/group_tracking.py
|
||||
:literal:
|
||||
|
||||
With this file in your working directory you can run the bot
|
||||
by specifying a database path, an e-mail address and password of
|
||||
by specifying a database path, an email address and password of
|
||||
a SMTP-IMAP account::
|
||||
|
||||
python group_tracking.py --email ADDRESS --password PASSWORD /tmp/db
|
||||
80
python/doc/cffi/install.rst
Normal file
80
python/doc/cffi/install.rst
Normal file
@@ -0,0 +1,80 @@
|
||||
Install
|
||||
=======
|
||||
|
||||
Installing pre-built packages (Linux-only)
|
||||
------------------------------------------
|
||||
|
||||
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 create a fresh Python virtual environment
|
||||
and activate it in your shell::
|
||||
|
||||
python -m venv env
|
||||
source env/bin/activate
|
||||
|
||||
Afterwards, invoking ``python`` or ``pip install`` only
|
||||
modifies files in your ``env`` directory and leaves
|
||||
your system installation alone.
|
||||
|
||||
For Linux we build wheels for all releases and push them to a python package
|
||||
index. To install the latest release::
|
||||
|
||||
pip install deltachat
|
||||
|
||||
To verify it worked::
|
||||
|
||||
python -c "import deltachat"
|
||||
|
||||
.. _sourceinstall:
|
||||
|
||||
Installing bindings from source
|
||||
-------------------------------
|
||||
|
||||
Install Rust and Cargo first.
|
||||
The easiest is probably to use `rustup <https://rustup.rs/>`_.
|
||||
|
||||
Bootstrap Rust and Cargo by using rustup::
|
||||
|
||||
curl https://sh.rustup.rs -sSf | sh
|
||||
|
||||
Then clone the deltachat-core-rust repo::
|
||||
|
||||
git clone https://github.com/deltachat/deltachat-core-rust
|
||||
cd deltachat-core-rust
|
||||
|
||||
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.
|
||||
|
||||
First, build the core library::
|
||||
|
||||
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
|
||||
|
||||
Build and install the bindings::
|
||||
|
||||
export DCC_RS_DEV="$PWD"
|
||||
export DCC_RS_TARGET=release
|
||||
python -m pip install ./python
|
||||
|
||||
`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.
|
||||
11
python/doc/cffi/intro.rst
Normal file
11
python/doc/cffi/intro.rst
Normal file
@@ -0,0 +1,11 @@
|
||||
Introduction
|
||||
============
|
||||
|
||||
CFFI bindings are available via the `deltachat <https://pypi.org/project/deltachat/>`_ Python package.
|
||||
The package contains both the Python bindings and the Delta Chat core.
|
||||
It is provided only for Linux.
|
||||
|
||||
The ``deltachat`` Python package provides two layers of bindings for the
|
||||
core Rust-library of the https://delta.chat messaging ecosystem:
|
||||
low-level CFFI bindings to the C interface of the Delta Chat core
|
||||
and high-level Python bindings built on top of CFFI bindings.
|
||||
@@ -1,8 +1,7 @@
|
||||
Low Level API Reference
|
||||
=======================
|
||||
|
||||
low level API reference
|
||||
===================================
|
||||
|
||||
for full doxygen-generated C-docs, defines and functions please checkout
|
||||
For full doxygen-generated C-docs, defines and functions please checkout
|
||||
|
||||
https://c.delta.chat
|
||||
|
||||
25
python/doc/cffi/manylinux.rst
Normal file
25
python/doc/cffi/manylinux.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
Building Manylinux-Based Wheels
|
||||
===============================
|
||||
|
||||
Building portable manylinux wheels which come with libdeltachat.so
|
||||
can be done with Docker_ or Podman_.
|
||||
|
||||
.. _Docker: https://www.docker.com/
|
||||
.. _Podman: https://podman.io/
|
||||
|
||||
If you want to build your own wheels, build container image first::
|
||||
|
||||
$ cd deltachat-core-rust # cd to deltachat-core-rust working tree
|
||||
$ docker build -t deltachat/coredeps scripts/coredeps
|
||||
|
||||
This will use the ``scripts/coredeps/Dockerfile`` to build
|
||||
container image called ``deltachat/coredeps``. You can afterwards
|
||||
find it with::
|
||||
|
||||
$ docker images
|
||||
|
||||
This docker image can be used to run tests and build Python wheels for all interpreters::
|
||||
|
||||
$ docker run -e CHATMAIL_DOMAIN \
|
||||
--rm -it -v $(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps scripts/run_all.sh
|
||||
49
python/doc/cffi/tests.rst
Normal file
49
python/doc/cffi/tests.rst
Normal file
@@ -0,0 +1,49 @@
|
||||
Running Tests
|
||||
=============
|
||||
|
||||
Recommended way to run tests is using `scripts/run-python-test.sh`
|
||||
script provided in the core repository.
|
||||
|
||||
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 email servers.
|
||||
|
||||
.. _`tox`: https://tox.wiki
|
||||
.. _livetests:
|
||||
|
||||
Running "Live" Tests With Temporary Accounts
|
||||
--------------------------------------------
|
||||
|
||||
If you want to run live functional tests
|
||||
you can set ``CHATMAIL_DOMAIN`` to a domain of the email server
|
||||
that creates email accounts like this::
|
||||
|
||||
export CHATMAIL_DOMAIN=nine.testrun.org
|
||||
|
||||
With this account-creation setting, pytest runs create ephemeral email accounts on the server.
|
||||
These accounts have the pattern `ci-{6 characters}@{CHATMAIL_DOMAIN}`.
|
||||
After setting the variable, either rerun `scripts/run-python-test.sh`
|
||||
or run offline and online tests with `tox` directly::
|
||||
|
||||
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`::
|
||||
|
||||
export DCC_RS_DEV="$PWD"
|
||||
export DCC_RS_TARGET=debug
|
||||
tox -c python --devenv env -e py
|
||||
. 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.
|
||||
@@ -1,4 +0,0 @@
|
||||
Changelog for deltachat-core's Python bindings
|
||||
==============================================
|
||||
|
||||
.. include:: ../CHANGELOG
|
||||
@@ -1,138 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# devpi documentation build configuration file, created by
|
||||
# sphinx-quickstart on Mon Jun 3 16:11:22 2013.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
from deltachat import __version__ as release
|
||||
|
||||
version = ".".join(release.split(".")[:2])
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.autosummary',
|
||||
#'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.viewcode',
|
||||
'breathe',
|
||||
#'sphinx.ext.githubpages',
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.autosummary",
|
||||
"sphinx.ext.viewcode",
|
||||
"breathe",
|
||||
"sphinx_rtd_theme",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = u'deltachat'
|
||||
copyright = u'2020, holger krekel and contributors'
|
||||
project = "Delta Chat"
|
||||
copyright = "2023, Delta Chat contributors"
|
||||
author = "Delta Chat contributors"
|
||||
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['sketch', '_build', "attic"]
|
||||
exclude_patterns = ["sketch", "_build", "attic", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
# modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- breathe options ------
|
||||
|
||||
breathe_projects = {
|
||||
"deltachat": "../../docs/xml/"
|
||||
}
|
||||
breathe_projects = {"deltachat": Path("../../docs/xml/")}
|
||||
|
||||
breathe_default_project = "deltachat"
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
||||
sys.path.append(os.path.abspath('_themes'))
|
||||
html_theme_path = ['_themes']
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
# html_theme = 'flask'
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
html_theme_options = {
|
||||
'logo': '_static/delta-chat.svg',
|
||||
'font_size': "1.1em",
|
||||
'caption_font_size': "0.9em",
|
||||
'code_font_size': "1.1em",
|
||||
|
||||
|
||||
}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = ["_themes"]
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
# html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
@@ -141,51 +97,34 @@ html_logo = "_static/delta-chat.svg"
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
html_favicon = '_static/favicon.ico'
|
||||
html_favicon = "_static/favicon.ico"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
html_static_path = ["_static"]
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
# html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#
|
||||
html_sidebars = {
|
||||
'index': [
|
||||
'sidebarintro.html',
|
||||
'globaltoc.html',
|
||||
'searchbox.html'
|
||||
],
|
||||
'**': [
|
||||
'sidebarintro.html',
|
||||
'globaltoc.html',
|
||||
'relations.html',
|
||||
'searchbox.html'
|
||||
]
|
||||
}
|
||||
|
||||
# html_use_smartypants = True
|
||||
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
html_show_sourcelink = False
|
||||
@@ -194,71 +133,65 @@ html_show_sourcelink = False
|
||||
html_show_sphinx = False
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
html_use_opensearch = 'https://doc.devpi.net'
|
||||
html_use_opensearch = "https://doc.devpi.net"
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
# html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'deltachat-python'
|
||||
htmlhelp_basename = "deltachat-python"
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
'pointsize': '12pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '12pt',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'devpi.tex', u'deltachat documentation',
|
||||
u'holger krekel', 'manual'),
|
||||
("index", "devpi.tex", "deltachat documentation", "holger krekel", "manual"),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'deltachat', u'deltachat documentation',
|
||||
[u'holger krekel'], 1)
|
||||
]
|
||||
man_pages = [("index", "deltachat", "deltachat documentation", ["holger krekel"], 1)]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output ------------------------------------------------
|
||||
@@ -267,30 +200,38 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'devpi', u'devpi Documentation',
|
||||
u'holger krekel', 'devpi', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(
|
||||
"index",
|
||||
"devpi",
|
||||
"devpi Documentation",
|
||||
"holger krekel",
|
||||
"devpi",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
# texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
# texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
# texinfo_show_urls = 'footnote'
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {'http://docs.python.org/': None}
|
||||
intersphinx_mapping = {"http://docs.python.org/": None}
|
||||
|
||||
# autodoc options
|
||||
autodoc_member_order = "bysource"
|
||||
|
||||
|
||||
# always document __init__ functions
|
||||
def skip(app, what, name, obj, skip, options):
|
||||
return skip
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.connect("autodoc-skip-member", skip)
|
||||
|
||||
|
||||
@@ -1,41 +1,44 @@
|
||||
deltachat python bindings
|
||||
=========================
|
||||
Delta Chat Python bindings, new and old
|
||||
=======
|
||||
|
||||
The ``deltachat`` Python package provides two layers of bindings for the
|
||||
core Rust-library of the https://delta.chat messaging ecosystem:
|
||||
|
||||
- :doc:`api` is a high level interface to deltachat-core.
|
||||
|
||||
- :doc:`plugins` is a brief introduction into implementing plugin hooks.
|
||||
|
||||
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
|
||||
<https://github.com/deltachat/deltachat-core-rust>`_.
|
||||
|
||||
|
||||
|
||||
getting started
|
||||
---------------
|
||||
`Delta Chat <https://delta.chat/>`_ provides two kinds of Python bindings
|
||||
to the `Rust Core <https://github.com/deltachat/deltachat-core-rust>`_:
|
||||
JSON-RPC bindings and CFFI bindings.
|
||||
When starting a new project it is recommended to use JSON-RPC bindings,
|
||||
which are used in the Delta Chat Desktop app through generated Typescript-bindings.
|
||||
The Python JSON-RPC bindings are maintained by Delta Chat core developers.
|
||||
Most existing bot projects and many tests in Delta Chat's own core library
|
||||
still use the CFFI-bindings, and it is going to be maintained certainly also in 2024.
|
||||
New APIs might however only appear in the JSON-RPC bindings,
|
||||
as the CFFI bindings are increasingly in maintenance-only mode.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: JSON-RPC Bindings
|
||||
|
||||
install
|
||||
examples
|
||||
jsonrpc/intro
|
||||
jsonrpc/install
|
||||
jsonrpc/examples
|
||||
jsonrpc/reference
|
||||
jsonrpc/develop
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
:maxdepth: 2
|
||||
:caption: CFFI Bindings
|
||||
|
||||
links
|
||||
changelog
|
||||
api
|
||||
lapi
|
||||
plugins
|
||||
|
||||
..
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
cffi/intro
|
||||
cffi/install
|
||||
cffi/examples
|
||||
cffi/manylinux
|
||||
cffi/tests
|
||||
cffi/api
|
||||
cffi/lapi
|
||||
cffi/plugins
|
||||
|
||||
.. _`deltachat`: https://delta.chat
|
||||
.. _`deltachat-core repo`: https://github.com/deltachat
|
||||
.. _pip: http://pypi.org/project/pip/
|
||||
.. _virtualenv: http://pypi.org/project/virtualenv/
|
||||
.. _merlinux: http://merlinux.eu
|
||||
.. _pypi: http://pypi.org/
|
||||
.. _`issue-tracker`: https://github.com/deltachat/deltachat-core-rust
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
.. include:: ../README.rst
|
||||
68
python/doc/jsonrpc/develop.rst
Normal file
68
python/doc/jsonrpc/develop.rst
Normal file
@@ -0,0 +1,68 @@
|
||||
===========
|
||||
Development
|
||||
===========
|
||||
|
||||
To develop JSON-RPC bindings,
|
||||
clone the `deltachat-core-rust <https://github.com/deltachat/deltachat-core-rust/>`_ repository::
|
||||
|
||||
git clone https://github.com/deltachat/deltachat-core-rust.git
|
||||
|
||||
Testing
|
||||
=======
|
||||
|
||||
To run online tests, set ``CHATMAIL_DOMAIN``
|
||||
to a domain of the email server
|
||||
that can be used to create testing accounts::
|
||||
|
||||
export CHATMAIL_DOMAIN=nine.testrun.org
|
||||
|
||||
Then run ``scripts/run-rpc-test.sh``
|
||||
to build debug version of ``deltachat-rpc-server``
|
||||
and run ``deltachat-rpc-client`` tests
|
||||
in a separate virtual environment managed by `tox <https://tox.wiki/>`_.
|
||||
|
||||
Development Environment
|
||||
=======================
|
||||
|
||||
Creating a new virtual environment
|
||||
to run the tests each time
|
||||
as ``scripts/run-rpc-test.sh`` does is slow
|
||||
if you are changing the tests or the code
|
||||
and want to rerun the tests each time.
|
||||
|
||||
If you are developing the tests,
|
||||
it is better to create a persistent virtual environment.
|
||||
You can do this by running ``scripts/make-rpc-testenv.sh``.
|
||||
This creates a virtual environment ``venv`` which you can then enter with::
|
||||
|
||||
. venv/bin/activate
|
||||
|
||||
Then you can run the tests with
|
||||
|
||||
::
|
||||
|
||||
pytest deltachat-rpc-client/tests/
|
||||
|
||||
Refer to `pytest documentation <https://docs.pytest.org/>` for details.
|
||||
|
||||
If make the changes to Delta Chat core
|
||||
or Python bindings, you can rebuild the environment by rerunning
|
||||
``scripts/make-rpc-testenv.sh``.
|
||||
It is ok to rebuild the activated environment this way,
|
||||
you do not need to deactivate or reactivate the environment each time.
|
||||
|
||||
Using REPL
|
||||
==========
|
||||
|
||||
Once you have a development environment,
|
||||
you can quickly test things in REPL::
|
||||
|
||||
$ python
|
||||
>>> from deltachat_rpc_client import *
|
||||
>>> rpc = Rpc()
|
||||
>>> rpc.start()
|
||||
>>> dc = DeltaChat(rpc)
|
||||
>>> system_info = dc.get_system_info()
|
||||
>>> system_info["level"]
|
||||
'awesome'
|
||||
>>> rpc.close()
|
||||
19
python/doc/jsonrpc/examples.rst
Normal file
19
python/doc/jsonrpc/examples.rst
Normal file
@@ -0,0 +1,19 @@
|
||||
Examples
|
||||
========
|
||||
|
||||
Echo bot
|
||||
--------
|
||||
.. include:: ../../../deltachat-rpc-client/examples/echobot_no_hooks.py
|
||||
:literal:
|
||||
|
||||
Echo bot with hooks
|
||||
-------------------
|
||||
.. include:: ../../../deltachat-rpc-client/examples/echobot.py
|
||||
:literal:
|
||||
|
||||
Advanced echo bot
|
||||
-----------------
|
||||
|
||||
.. include:: ../../../deltachat-rpc-client/examples/echobot_advanced.py
|
||||
:literal:
|
||||
|
||||
36
python/doc/jsonrpc/install.rst
Normal file
36
python/doc/jsonrpc/install.rst
Normal file
@@ -0,0 +1,36 @@
|
||||
Install
|
||||
=======
|
||||
|
||||
To use JSON-RPC bindings for Delta Chat core you will need
|
||||
a ``deltachat-rpc-server`` binary which provides Delta Chat core API over JSON-RPC
|
||||
and a ``deltachat-rpc-client`` Python package which is a JSON-RPC client that starts ``deltachat-rpc-server`` process and uses JSON-RPC API.
|
||||
|
||||
`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 venv
|
||||
$ . venv/bin/activate
|
||||
|
||||
Install ``deltachat-rpc-server``
|
||||
--------------------------------
|
||||
|
||||
To get ``deltachat-rpc-server`` binary you have three options:
|
||||
|
||||
1. Install ``deltachat-rpc-server`` from PyPI using ``pip install deltachat-rpc-server``.
|
||||
2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --git https://github.com/deltachat/deltachat-core-rust/ deltachat-rpc-server``.
|
||||
3. Download prebuilt release from https://github.com/deltachat/deltachat-core-rust/releases and install it into ``PATH``.
|
||||
|
||||
Check that ``deltachat-rpc-server`` is installed and can run::
|
||||
|
||||
$ deltachat-rpc-server --version
|
||||
1.131.4
|
||||
|
||||
Then install ``deltachat-rpc-client`` with ``pip install deltachat-rpc-client``.
|
||||
|
||||
Install ``deltachat-rpc-client``
|
||||
--------------------------------
|
||||
|
||||
To get ``deltachat-rpc-client`` Python library you can:
|
||||
|
||||
1. Install ``deltachat-rpc-client`` from PyPI using ``pip install deltachat-rpc-client``.
|
||||
2. Install ``deltachat-rpc-client`` from source with ``pip install git+https://github.com/deltachat/deltachat-core-rust.git@main#subdirectory=deltachat-rpc-client``.
|
||||
8
python/doc/jsonrpc/intro.rst
Normal file
8
python/doc/jsonrpc/intro.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
Introduction
|
||||
============
|
||||
|
||||
JSON-RPC bindings are available via the `deltachat-rpc-client <https://pypi.org/project/deltachat-rpc-client/>`_ Python package.
|
||||
This package provides only the Python bindings and requires ``deltachat-rpc-server`` binary to be installed.
|
||||
`deltachat-rpc-server <https://pypi.org/project/deltachat-rpc-server/>`_ package provides ``deltachat-rpc-server`` binary for Linux, Windows, macOS and Android.
|
||||
|
||||
RPC client connects to standalone Delta Chat RPC server ``deltachat-rpc-server`` and provides Python interface to it.
|
||||
5
python/doc/jsonrpc/reference.rst
Normal file
5
python/doc/jsonrpc/reference.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
API Reference
|
||||
=============
|
||||
|
||||
.. automodule:: deltachat_rpc_client
|
||||
:members:
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
links
|
||||
================================
|
||||
|
||||
.. _`deltachat`: https://delta.chat
|
||||
.. _`deltachat-core repo`: https://github.com/deltachat
|
||||
.. _pip: http://pypi.org/project/pip/
|
||||
.. _virtualenv: http://pypi.org/project/virtualenv/
|
||||
.. _merlinux: http://merlinux.eu
|
||||
.. _pypi: http://pypi.org/
|
||||
.. _`issue-tracker`: https://github.com/deltachat/deltachat-core
|
||||
@@ -1,190 +0,0 @@
|
||||
@ECHO OFF
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set BUILDDIR=_build
|
||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||
echo. html to make standalone HTML files
|
||||
echo. dirhtml to make HTML files named index.html in directories
|
||||
echo. singlehtml to make a single large HTML file
|
||||
echo. pickle to make pickle files
|
||||
echo. json to make JSON files
|
||||
echo. htmlhelp to make HTML files and a HTML help project
|
||||
echo. qthelp to make HTML files and a qthelp project
|
||||
echo. devhelp to make HTML files and a Devhelp project
|
||||
echo. epub to make an epub
|
||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||
echo. text to make text files
|
||||
echo. man to make manual pages
|
||||
echo. texinfo to make Texinfo files
|
||||
echo. gettext to make PO message catalogs
|
||||
echo. changes to make an overview over all changed/added/deprecated items
|
||||
echo. linkcheck to check all external links for integrity
|
||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||
del /q /s %BUILDDIR%\*
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "singlehtml" (
|
||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\devpi.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\devpi.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "devhelp" (
|
||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub" (
|
||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "text" (
|
||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "man" (
|
||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "texinfo" (
|
||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "gettext" (
|
||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.The overview file is in %BUILDDIR%/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in %BUILDDIR%/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
||||
@@ -140,6 +140,10 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
assert msg.is_system_message()
|
||||
assert "added" in msg.text.lower()
|
||||
|
||||
assert any(
|
||||
m.is_system_message() and m.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
for m in msg.chat.get_messages()
|
||||
)
|
||||
lp.sec("ac1: send message")
|
||||
msg_out = chat1.send_text("hello")
|
||||
assert msg_out.is_encrypted()
|
||||
@@ -580,6 +584,7 @@ def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
|
||||
assert msg_in.get_sender_contact().addr == ac1.get_config("addr")
|
||||
chat2 = msg_in.chat
|
||||
assert chat2.is_protected()
|
||||
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
|
||||
lp.sec("ac2_offl: sending message")
|
||||
msg_out = chat2.send_text("hello")
|
||||
@@ -647,6 +652,15 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
chat2.send_text("hi2")
|
||||
|
||||
lp.sec("ac2_offl: receiving message")
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert msg_in.is_system_message()
|
||||
assert msg_in.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
|
||||
# We need to consume one event that has data2=0
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
assert ev.data2 == 0
|
||||
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert not msg_in.is_system_message()
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytest
|
||||
from imap_tools import AND, U
|
||||
|
||||
import deltachat as dc
|
||||
from deltachat import account_hookimpl, Message, Chat
|
||||
from deltachat import account_hookimpl, Message
|
||||
from deltachat.tracker import ImexTracker
|
||||
|
||||
|
||||
@@ -1658,128 +1658,6 @@ def test_ac_setup_message_twice(acfactory, lp):
|
||||
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
|
||||
|
||||
|
||||
def test_qr_setup_contact(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
|
||||
qr = ac1.get_setup_contact_qr()
|
||||
|
||||
lp.sec("ac2: start QR-code based setup contact protocol")
|
||||
ch = ac2.qr_setup_contact(qr)
|
||||
assert ch.id >= 10
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("verified_one_on_one_chats", [0, 1])
|
||||
def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
|
||||
ac2.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
|
||||
|
||||
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
|
||||
chat = ac1.create_group_chat("hello")
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
ch = ac2.qr_join_chat(qr)
|
||||
lp.sec("ac2: qr_join_chat() returned")
|
||||
assert ch.id >= 10
|
||||
# check that at least some of the handshake messages are deleted
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "Member Me ({}) added by {}.".format(ac2.get_config("addr"), ac1.get_config("addr"))
|
||||
|
||||
# ac1 reloads the chat.
|
||||
chat = Chat(chat.account, chat.id)
|
||||
assert not chat.is_protected()
|
||||
|
||||
# ac2 reloads the chat.
|
||||
ch = Chat(ch.account, ch.id)
|
||||
assert not ch.is_protected()
|
||||
|
||||
|
||||
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory, lp):
|
||||
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
||||
|
||||
lp.sec("ac3: verify with ac2")
|
||||
ac3.qr_setup_contact(ac2.get_setup_contact_qr())
|
||||
ac2._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
# in order for ac2 to have pending bobstate with a verified group
|
||||
# we first create a fully joined verified group, and then start
|
||||
# joining a second time but interrupt it, to create pending bob state
|
||||
|
||||
lp.sec("ac1: create verified group that ac2 fully joins")
|
||||
ch1 = ac1.create_group_chat("ac1-shutoff group", verified=True)
|
||||
ac2.qr_join_chat(ch1.get_join_qr())
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
# ensure ac1 can write and ac2 receives messages in verified chat
|
||||
ch1.send_text("ac1 says hello")
|
||||
while 1:
|
||||
msg = ac2.wait_next_incoming_message()
|
||||
if msg.text == "ac1 says hello":
|
||||
assert msg.chat.is_protected()
|
||||
break
|
||||
|
||||
lp.sec("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
|
||||
ac2.qr_join_chat(ch1.get_join_qr())
|
||||
ac1.shutdown()
|
||||
lp.sec("ac2 now has pending bobstate but ac1 is shutoff")
|
||||
|
||||
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
|
||||
assert ac3.get_contact(ac2).is_verified()
|
||||
assert ac2.get_contact(ac3).is_verified()
|
||||
|
||||
lp.sec("ac3: create a verified group VG with ac2")
|
||||
vg = ac3.create_group_chat("ac3-created", [ac2], verified=True)
|
||||
|
||||
# ensure ac2 receives message in VG
|
||||
vg.send_text("hello")
|
||||
while 1:
|
||||
msg = ac2.wait_next_incoming_message()
|
||||
if msg.text == "hello":
|
||||
assert msg.chat.is_protected()
|
||||
break
|
||||
|
||||
lp.sec("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
|
||||
ac4.qr_join_chat(vg.get_join_qr())
|
||||
ac3._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
while 1:
|
||||
ev = ac2._evtracker.get()
|
||||
if "added by unrelated SecureJoin" in str(ev):
|
||||
return
|
||||
|
||||
|
||||
def test_qr_new_group_unblocked(acfactory, lp):
|
||||
"""Regression test for a bug intoduced in core v1.113.0.
|
||||
ac2 scans a verified group QR code created by ac1.
|
||||
This results in creation of a blocked 1:1 chat with ac1 on ac2,
|
||||
but ac1 contact is not blocked on ac2.
|
||||
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
|
||||
ac2 should receive a message and create a contact request for the group.
|
||||
Due to a bug previously ac2 created a blocked group.
|
||||
"""
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_chat = ac1.create_group_chat("Group for joining", verified=True)
|
||||
qr = ac1_chat.get_join_qr()
|
||||
ac2.qr_join_chat(qr)
|
||||
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
ac1_new_chat = ac1.create_group_chat("Another group")
|
||||
ac1_new_chat.add_contact(ac2)
|
||||
# Receive "Member added" message.
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
ac1_new_chat.send_text("Hello!")
|
||||
ac2_msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_msg.text == "Hello!"
|
||||
assert ac2_msg.chat.is_contact_request()
|
||||
|
||||
|
||||
def test_qr_email_capitalization(acfactory, lp):
|
||||
"""Regression test for a bug
|
||||
that resulted in failure to propagate verification via gossip in a verified group
|
||||
@@ -2527,47 +2405,6 @@ def test_delete_deltachat_folder(acfactory):
|
||||
assert "DeltaChat" in ac1.direct_imap.list_folders()
|
||||
|
||||
|
||||
def test_aeap_flow_verified(acfactory, lp):
|
||||
"""Test that a new address is added to a contact when it changes its address."""
|
||||
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat.is_protected()
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
assert chat2.id >= 10
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
lp.sec("sending first message")
|
||||
msg_out = chat.send_text("old address")
|
||||
|
||||
lp.sec("receiving first message")
|
||||
ac2._evtracker.wait_next_incoming_message() # member added message
|
||||
msg_in_1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in_1.text == msg_out.text
|
||||
|
||||
lp.sec("changing email account")
|
||||
ac1.set_config("addr", ac1new.get_config("addr"))
|
||||
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
|
||||
ac1.stop_io()
|
||||
configtracker = ac1.configure()
|
||||
configtracker.wait_finish()
|
||||
ac1.start_io()
|
||||
|
||||
lp.sec("sending second message")
|
||||
msg_out = chat.send_text("changed address")
|
||||
|
||||
lp.sec("receiving second message")
|
||||
msg_in_2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in_2.text == msg_out.text
|
||||
assert msg_in_2.chat.id == msg_in_1.chat.id
|
||||
assert msg_in_2.get_sender_contact().addr == ac1new.get_config("addr")
|
||||
assert len(msg_in_2.chat.get_contacts()) == 2
|
||||
assert ac1new.get_config("addr") in [contact.addr for contact in msg_in_2.chat.get_contacts()]
|
||||
|
||||
|
||||
def test_archived_muted_chat(acfactory, lp):
|
||||
"""If an archived and muted chat receives a new message, DC_EVENT_MSGS_CHANGED for
|
||||
DC_CHAT_ID_ARCHIVED_LINK must be generated if the chat had only seen messages previously.
|
||||
|
||||
@@ -62,9 +62,9 @@ commands =
|
||||
[testenv:doc]
|
||||
changedir=doc
|
||||
deps =
|
||||
# Pinned due to incompatibility of breathe with sphinx 7.2: <https://github.com/breathe-doc/breathe/issues/943>
|
||||
sphinx<=7.1.2
|
||||
sphinx
|
||||
breathe
|
||||
sphinx_rtd_theme
|
||||
commands =
|
||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
2023-11-10
|
||||
2023-11-20
|
||||
@@ -39,6 +39,8 @@ and an own build machine.
|
||||
|
||||
- `android-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Android NDK.
|
||||
|
||||
- `build-python-docs.sh` builds Python documentation into `dist/html/`.
|
||||
|
||||
## Triggering runs on the build machine locally (fast!)
|
||||
|
||||
There is experimental support for triggering a remote Python or Rust test run
|
||||
|
||||
13
scripts/build-python-docs.sh
Executable file
13
scripts/build-python-docs.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export DCC_RS_TARGET=debug
|
||||
export DCC_RS_DEV="$PWD"
|
||||
cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
python3 -m venv venv
|
||||
venv/bin/pip install ./python
|
||||
venv/bin/pip install ./deltachat-rpc-client
|
||||
venv/bin/pip install sphinx breathe sphinx_rtd_theme
|
||||
venv/bin/pip install ./deltachat-rpc-client
|
||||
venv/bin/sphinx-build -b html -a python/doc/ dist/html
|
||||
@@ -102,8 +102,6 @@ jobs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
- name: py-docs
|
||||
path: ./python/doc/_build/
|
||||
# Binary wheels
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
@@ -115,28 +113,6 @@ jobs:
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload python docs to py.delta.chat
|
||||
- task: upload-py-docs
|
||||
config:
|
||||
inputs:
|
||||
- name: py-docs
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: alpine
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apk add --no-cache rsync openssh-client
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete py-docs/html/ delta@py.delta.chat:build/master
|
||||
|
||||
# Upload x86_64 wheels and source packages
|
||||
- task: upload-wheels
|
||||
config:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Build the Delta Chat Core Rust library, Python wheels and docs
|
||||
# Build the Delta Chat Core Rust library and Python wheels
|
||||
|
||||
set -e -x
|
||||
|
||||
@@ -34,9 +34,3 @@ unset CHATMAIL_DOMAIN
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
|
||||
|
||||
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
|
||||
|
||||
|
||||
echo -----------------------
|
||||
echo generating python docs
|
||||
echo -----------------------
|
||||
tox --workdir "$TOXWORKDIR" -e doc
|
||||
|
||||
@@ -67,7 +67,7 @@ impl Aheader {
|
||||
|
||||
impl fmt::Display for Aheader {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(fmt, "addr={};", self.addr)?;
|
||||
write!(fmt, "addr={};", self.addr.to_lowercase())?;
|
||||
if self.prefer_encrypt == EncryptPreference::Mutual {
|
||||
write!(fmt, " prefer-encrypt=mutual;")?;
|
||||
}
|
||||
@@ -262,5 +262,16 @@ mod tests {
|
||||
)
|
||||
)
|
||||
.contains("prefer-encrypt"));
|
||||
|
||||
// Always lowercase the address in the header.
|
||||
assert!(format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"TeSt@eXaMpLe.cOm".to_string(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::Mutual
|
||||
)
|
||||
)
|
||||
.contains("test@example.com"));
|
||||
}
|
||||
}
|
||||
|
||||
562
src/chat.rs
562
src/chat.rs
@@ -1,13 +1,13 @@
|
||||
//! # Chat module.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use anyhow::{anyhow, bail, ensure, Context as _, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::EnumIter;
|
||||
@@ -21,7 +21,7 @@ use crate::constants::{
|
||||
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
|
||||
DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS,
|
||||
};
|
||||
use crate::contact::{self, Contact, ContactId, Origin, VerifiedStatus};
|
||||
use crate::contact::{self, Contact, ContactAddress, ContactId, Origin, VerifiedStatus};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc;
|
||||
use crate::download::DownloadState;
|
||||
@@ -29,6 +29,7 @@ use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::events::EventType;
|
||||
use crate::html::new_html_mimepart;
|
||||
use crate::location;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -38,11 +39,11 @@ use crate::receive_imf::ReceivedMsg;
|
||||
use crate::smtp::send_msg_to_smtp;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::sync::{self, ChatAction, Sync::*, SyncData};
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
|
||||
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
|
||||
strip_rtlo_characters, time, IsNoneOrEmpty,
|
||||
smeared_time, strip_rtlo_characters, time, IsNoneOrEmpty,
|
||||
};
|
||||
use crate::webxdc::WEBXDC_SUFFIX;
|
||||
|
||||
@@ -399,7 +400,10 @@ impl ChatId {
|
||||
|
||||
if sync.into() {
|
||||
// NB: For a 1:1 chat this currently triggers `Contact::block()` on other devices.
|
||||
chat.add_sync_item(context, ChatAction::Block).await?;
|
||||
chat.sync(context, SyncAction::Block)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -417,7 +421,10 @@ impl ChatId {
|
||||
// TODO: For a 1:1 chat this currently triggers `Contact::unblock()` on other devices.
|
||||
// Maybe we should unblock the contact locally too, this would also resolve discrepancy
|
||||
// with `block()` which also blocks the contact.
|
||||
chat.add_sync_item(context, ChatAction::Unblock).await?;
|
||||
chat.sync(context, SyncAction::Unblock)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -465,7 +472,10 @@ impl ChatId {
|
||||
}
|
||||
|
||||
if sync.into() {
|
||||
chat.add_sync_item(context, ChatAction::Accept).await?;
|
||||
chat.sync(context, SyncAction::Accept)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -567,6 +577,28 @@ impl ChatId {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the 1:1 chat with the given address to ProtectionStatus::Protected,
|
||||
/// and posts a `SystemMessage::ChatProtectionEnabled` into it.
|
||||
///
|
||||
/// If necessary, creates a hidden chat for this.
|
||||
pub(crate) async fn set_protection_for_contact(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
) -> Result<()> {
|
||||
let chat_id = ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Yes)
|
||||
.await
|
||||
.with_context(|| format!("can't create chat for {}", contact_id))?;
|
||||
chat_id
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
smeared_time(context),
|
||||
Some(contact_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Archives or unarchives a chat.
|
||||
pub async fn set_visibility(self, context: &Context, visibility: ChatVisibility) -> Result<()> {
|
||||
self.set_visibility_ex(context, Sync, visibility).await
|
||||
@@ -605,8 +637,10 @@ impl ChatId {
|
||||
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
chat.add_sync_item(context, ChatAction::SetVisibility(visibility))
|
||||
.await?;
|
||||
chat.sync(context, SyncAction::SetVisibility(visibility))
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -959,8 +993,9 @@ impl ChatId {
|
||||
AND y.contact_id > 9
|
||||
AND x.chat_id=?
|
||||
AND y.chat_id<>x.chat_id
|
||||
AND y.chat_id>?
|
||||
GROUP BY y.chat_id",
|
||||
(self,),
|
||||
(self, DC_CHAT_ID_LAST_SPECIAL),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let intersection: f64 = row.get(1)?;
|
||||
@@ -1697,7 +1732,11 @@ impl Chat {
|
||||
// send_sync_msg() is called (usually) a moment later at send_msg_to_smtp()
|
||||
// when the group-creation message is actually sent though SMTP -
|
||||
// this makes sure, the other devices are aware of grpid that is used in the sync-message.
|
||||
context.sync_qr_code_tokens(Some(self.id)).await?;
|
||||
context
|
||||
.sync_qr_code_tokens(Some(self.id))
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
|
||||
// reset encrypt error state eg. for forwarding
|
||||
@@ -1894,8 +1933,25 @@ impl Chat {
|
||||
Ok(msg.id)
|
||||
}
|
||||
|
||||
/// Sends a `SyncAction` synchronising chat contacts to other devices.
|
||||
pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> {
|
||||
let addrs = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.addr \
|
||||
FROM contacts c INNER JOIN chats_contacts cc \
|
||||
ON c.id=cc.contact_id \
|
||||
WHERE cc.chat_id=?",
|
||||
(self.id,),
|
||||
|row| row.get::<_, String>(0),
|
||||
|addrs| addrs.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
self.sync(context, SyncAction::SetContacts(addrs)).await
|
||||
}
|
||||
|
||||
/// Returns chat id for the purpose of synchronisation across devices.
|
||||
async fn get_sync_id(&self, context: &Context) -> Result<Option<sync::ChatId>> {
|
||||
async fn get_sync_id(&self, context: &Context) -> Result<Option<SyncId>> {
|
||||
match self.typ {
|
||||
Chattype::Single => {
|
||||
let mut r = None;
|
||||
@@ -1907,7 +1963,7 @@ impl Chat {
|
||||
return Ok(None);
|
||||
}
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
r = Some(sync::ChatId::ContactAddr(contact.get_addr().to_string()));
|
||||
r = Some(SyncId::ContactAddr(contact.get_addr().to_string()));
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
@@ -1915,23 +1971,28 @@ impl Chat {
|
||||
if self.grpid.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(sync::ChatId::Grpid(self.grpid.clone())))
|
||||
Ok(Some(SyncId::Grpid(self.grpid.clone())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a chat action to the list of items to synchronise to other devices.
|
||||
pub(crate) async fn add_sync_item(&self, context: &Context, action: ChatAction) -> Result<()> {
|
||||
/// Synchronises a chat action to other devices.
|
||||
pub(crate) async fn sync(&self, context: &Context, action: SyncAction) -> Result<()> {
|
||||
if let Some(id) = self.get_sync_id(context).await? {
|
||||
context
|
||||
.add_sync_item(SyncData::AlterChat { id, action })
|
||||
.await?;
|
||||
context.send_sync_msg().await?;
|
||||
sync(context, id, action).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn sync(context: &Context, id: SyncId, action: SyncAction) -> Result<()> {
|
||||
context
|
||||
.add_sync_item(SyncData::AlterChat { id, action })
|
||||
.await?;
|
||||
context.send_sync_msg().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the chat is pinned or archived.
|
||||
#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize, EnumIter)]
|
||||
#[repr(i8)]
|
||||
@@ -2386,10 +2447,13 @@ async fn prepare_msg_common(
|
||||
|
||||
// Check if the chat can be sent to.
|
||||
if let Some(reason) = chat.why_cant_send(context).await? {
|
||||
if reason == CantSendReason::ProtectionBroken
|
||||
&& msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
if matches!(
|
||||
reason,
|
||||
CantSendReason::ProtectionBroken | CantSendReason::ContactRequest
|
||||
) && msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
{
|
||||
// Send out the message, the securejoin message is supposed to repair the verification
|
||||
// Send out the message, the securejoin message is supposed to repair the verification.
|
||||
// If the chat is a contact request, let the user accept it later.
|
||||
} else {
|
||||
bail!("cannot send to {chat_id}: {reason}");
|
||||
}
|
||||
@@ -3193,26 +3257,79 @@ async fn find_unused_broadcast_list_name(context: &Context) -> Result<String> {
|
||||
pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
|
||||
let chat_name = find_unused_broadcast_list_name(context).await?;
|
||||
let grpid = create_id();
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO chats
|
||||
(type, name, grpid, param, created_timestamp)
|
||||
VALUES(?, ?, ?, \'U=1\', ?);",
|
||||
(
|
||||
Chattype::Broadcast,
|
||||
chat_name,
|
||||
grpid,
|
||||
create_smeared_timestamp(context),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
create_broadcast_list_ex(context, Sync, grpid, chat_name).await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_broadcast_list_ex(
|
||||
context: &Context,
|
||||
sync: sync::Sync,
|
||||
grpid: String,
|
||||
chat_name: String,
|
||||
) -> Result<ChatId> {
|
||||
let row_id = {
|
||||
let chat_name = &chat_name;
|
||||
let grpid = &grpid;
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
let cnt = t.execute("UPDATE chats SET name=? WHERE grpid=?", (chat_name, grpid))?;
|
||||
ensure!(cnt <= 1, "{cnt} chats exist with grpid {grpid}");
|
||||
if cnt == 1 {
|
||||
return Ok(t.query_row(
|
||||
"SELECT id FROM chats WHERE grpid=? AND type=?",
|
||||
(grpid, Chattype::Broadcast),
|
||||
|row| {
|
||||
let id: isize = row.get(0)?;
|
||||
Ok(id)
|
||||
},
|
||||
)?);
|
||||
}
|
||||
t.execute(
|
||||
"INSERT INTO chats \
|
||||
(type, name, grpid, param, created_timestamp) \
|
||||
VALUES(?, ?, ?, \'U=1\', ?);",
|
||||
(
|
||||
Chattype::Broadcast,
|
||||
&chat_name,
|
||||
&grpid,
|
||||
create_smeared_timestamp(context),
|
||||
),
|
||||
)?;
|
||||
Ok(t.last_insert_rowid().try_into()?)
|
||||
};
|
||||
context.sql.transaction(trans_fn).await?
|
||||
};
|
||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
if sync.into() {
|
||||
let id = SyncId::Grpid(grpid);
|
||||
let action = SyncAction::CreateBroadcast(chat_name);
|
||||
self::sync(context, id, action).await.log_err(context).ok();
|
||||
}
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
/// Set chat contacts in the `chats_contacts` table.
|
||||
pub(crate) async fn update_chat_contacts_table(
|
||||
context: &Context,
|
||||
id: ChatId,
|
||||
contacts: &HashSet<ContactId>,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
|
||||
for contact_id in contacts {
|
||||
transaction.execute(
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
||||
(id, contact_id),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds contacts to the `chats_contacts` table.
|
||||
pub(crate) async fn add_to_chat_contacts_table(
|
||||
context: &Context,
|
||||
@@ -3258,12 +3375,13 @@ pub async fn add_contact_to_chat(
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
) -> Result<()> {
|
||||
add_contact_to_chat_ex(context, chat_id, contact_id, false).await?;
|
||||
add_contact_to_chat_ex(context, Sync, chat_id, contact_id, false).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn add_contact_to_chat_ex(
|
||||
context: &Context,
|
||||
mut sync: sync::Sync,
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
from_handshake: bool,
|
||||
@@ -3302,8 +3420,12 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
||||
chat.param.remove(Param::Unpromoted);
|
||||
chat.update_param(context).await?;
|
||||
context.sync_qr_code_tokens(Some(chat_id)).await?;
|
||||
context.send_sync_msg().await?;
|
||||
let _ = context
|
||||
.sync_qr_code_tokens(Some(chat_id))
|
||||
.await
|
||||
.log_err(context)
|
||||
.is_ok()
|
||||
&& context.send_sync_msg().await.log_err(context).is_ok();
|
||||
}
|
||||
|
||||
if context.is_self_addr(contact.get_addr()).await? {
|
||||
@@ -3339,14 +3461,18 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
if chat.typ == Chattype::Group && chat.is_promoted() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
|
||||
let contact_addr = contact.get_addr();
|
||||
msg.text = stock_str::msg_add_member_local(context, contact_addr, ContactId::SELF).await;
|
||||
let contact_addr = contact.get_addr().to_lowercase();
|
||||
msg.text = stock_str::msg_add_member_local(context, &contact_addr, ContactId::SELF).await;
|
||||
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
|
||||
msg.param.set(Param::Arg, contact_addr);
|
||||
msg.param.set_int(Param::Arg2, from_handshake.into());
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
sync = Nosync;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
if sync.into() {
|
||||
chat.sync_contacts(context).await.log_err(context).ok();
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -3453,8 +3579,10 @@ pub(crate) async fn set_muted_ex(
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
chat.add_sync_item(context, ChatAction::SetMuted(duration))
|
||||
.await?;
|
||||
chat.sync(context, SyncAction::SetMuted(duration))
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -3486,6 +3614,7 @@ pub async fn remove_contact_from_chat(
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
|
||||
bail!("{}", err_msg);
|
||||
} else {
|
||||
let mut sync = Nosync;
|
||||
// We do not return an error if the contact does not exist in the database.
|
||||
// This allows to delete dangling references to deleted contacts
|
||||
// in case of the database becoming inconsistent due to a bug.
|
||||
@@ -3504,8 +3633,10 @@ pub async fn remove_contact_from_chat(
|
||||
.await;
|
||||
}
|
||||
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
|
||||
msg.param.set(Param::Arg, contact.get_addr());
|
||||
msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
} else {
|
||||
sync = Sync;
|
||||
}
|
||||
}
|
||||
// we remove the member from the chat after constructing the
|
||||
@@ -3520,6 +3651,9 @@ pub async fn remove_contact_from_chat(
|
||||
// check/encryption logic.
|
||||
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
if sync.into() {
|
||||
chat.sync_contacts(context).await.log_err(context).ok();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bail!("Cannot remove members from non-group chats.");
|
||||
@@ -3549,6 +3683,15 @@ pub(crate) async fn is_group_explicitly_left(context: &Context, grpid: &str) ->
|
||||
|
||||
/// Sets group or mailing list chat name.
|
||||
pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -> Result<()> {
|
||||
rename_ex(context, Sync, chat_id, new_name).await
|
||||
}
|
||||
|
||||
async fn rename_ex(
|
||||
context: &Context,
|
||||
sync: sync::Sync,
|
||||
chat_id: ChatId,
|
||||
new_name: &str,
|
||||
) -> Result<()> {
|
||||
let new_name = improve_single_line_input(new_name);
|
||||
/* the function only sets the names of group chats; normal chats get their names from the contacts */
|
||||
let mut success = false;
|
||||
@@ -3600,7 +3743,13 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
|
||||
if !success {
|
||||
bail!("Failed to set name");
|
||||
}
|
||||
|
||||
if sync.into() && chat.name != new_name {
|
||||
let sync_name = new_name.to_string();
|
||||
chat.sync(context, SyncAction::Rename(sync_name))
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4074,15 +4223,59 @@ pub(crate) async fn update_msg_text_and_timestamp(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set chat contacts by their addresses creating the corresponding contacts if necessary.
|
||||
async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String]) -> Result<()> {
|
||||
let chat = Chat::load_from_db(context, id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast,
|
||||
"{id} is not a group/broadcast",
|
||||
);
|
||||
let mut contacts = HashSet::new();
|
||||
for addr in addrs {
|
||||
let contact_addr = ContactAddress::new(addr)?;
|
||||
let contact = Contact::add_or_lookup(context, "", &contact_addr, Origin::Hidden)
|
||||
.await?
|
||||
.0;
|
||||
contacts.insert(contact);
|
||||
}
|
||||
let contacts_old = HashSet::<ContactId>::from_iter(get_chat_contacts(context, id).await?);
|
||||
if contacts == contacts_old {
|
||||
return Ok(());
|
||||
}
|
||||
update_chat_contacts_table(context, id, &contacts).await?;
|
||||
context.emit_event(EventType::ChatModified(id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A cross-device chat id used for synchronisation.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub(crate) enum SyncId {
|
||||
ContactAddr(String),
|
||||
Grpid(String),
|
||||
// NOTE: Ad-hoc groups lack an identifier that can be used across devices so
|
||||
// block/mute/etc. actions on them are not synchronized to other devices.
|
||||
}
|
||||
|
||||
/// An action synchronised to other devices.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub(crate) enum SyncAction {
|
||||
Block,
|
||||
Unblock,
|
||||
Accept,
|
||||
SetVisibility(ChatVisibility),
|
||||
SetMuted(MuteDuration),
|
||||
/// Create broadcast list with the given name.
|
||||
CreateBroadcast(String),
|
||||
Rename(String),
|
||||
/// Set chat contacts by their addresses.
|
||||
SetContacts(Vec<String>),
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Executes [`SyncData::AlterChat`] item sent by other device.
|
||||
pub(crate) async fn sync_alter_chat(
|
||||
&self,
|
||||
id: &sync::ChatId,
|
||||
action: &ChatAction,
|
||||
) -> Result<()> {
|
||||
pub(crate) async fn sync_alter_chat(&self, id: &SyncId, action: &SyncAction) -> Result<()> {
|
||||
let chat_id = match id {
|
||||
sync::ChatId::ContactAddr(addr) => {
|
||||
SyncId::ContactAddr(addr) => {
|
||||
let Some(contact_id) =
|
||||
Contact::lookup_id_by_addr_ex(self, addr, Origin::Unknown, None).await?
|
||||
else {
|
||||
@@ -4090,10 +4283,10 @@ impl Context {
|
||||
return Ok(());
|
||||
};
|
||||
match action {
|
||||
ChatAction::Block => {
|
||||
SyncAction::Block => {
|
||||
return contact::set_blocked(self, Nosync, contact_id, true).await
|
||||
}
|
||||
ChatAction::Unblock => {
|
||||
SyncAction::Unblock => {
|
||||
return contact::set_blocked(self, Nosync, contact_id, false).await
|
||||
}
|
||||
_ => (),
|
||||
@@ -4104,7 +4297,11 @@ impl Context {
|
||||
};
|
||||
chat_id
|
||||
}
|
||||
sync::ChatId::Grpid(grpid) => {
|
||||
SyncId::Grpid(grpid) => {
|
||||
if let SyncAction::CreateBroadcast(name) = action {
|
||||
create_broadcast_list_ex(self, Nosync, grpid.clone(), name.clone()).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let Some((chat_id, ..)) = get_chat_id_by_grpid(self, grpid).await? else {
|
||||
warn!(self, "sync_alter_chat: No chat for grpid '{grpid}'.");
|
||||
return Ok(());
|
||||
@@ -4113,14 +4310,17 @@ impl Context {
|
||||
}
|
||||
};
|
||||
match action {
|
||||
ChatAction::Block => chat_id.block_ex(self, Nosync).await,
|
||||
ChatAction::Unblock => chat_id.unblock_ex(self, Nosync).await,
|
||||
ChatAction::Accept => chat_id.accept_ex(self, Nosync).await,
|
||||
ChatAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await,
|
||||
ChatAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await,
|
||||
SyncAction::Block => chat_id.block_ex(self, Nosync).await,
|
||||
SyncAction::Unblock => chat_id.unblock_ex(self, Nosync).await,
|
||||
SyncAction::Accept => chat_id.accept_ex(self, Nosync).await,
|
||||
SyncAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await,
|
||||
SyncAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await,
|
||||
SyncAction::CreateBroadcast(_) => {
|
||||
Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request."))
|
||||
}
|
||||
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
|
||||
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
|
||||
}
|
||||
.ok();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4133,6 +4333,7 @@ mod tests {
|
||||
use crate::message::delete_msgs;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use strum::IntoEnumIterator;
|
||||
use tokio::fs;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -4343,7 +4544,7 @@ mod tests {
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
let added = add_contact_to_chat_ex(&t, chat_id, ContactId::SELF, false)
|
||||
let added = add_contact_to_chat_ex(&t, Nosync, chat_id, ContactId::SELF, false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(added, false);
|
||||
@@ -4485,6 +4686,37 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that if a message implicitly adds a member, both messages appear.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_msg_with_implicit_member_add() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let alice_bob_contact_id =
|
||||
Contact::create(&alice, "Bob", &bob.get_config(Config::Addr).await?.unwrap()).await?;
|
||||
let fiona_addr = "fiona@example.net";
|
||||
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", fiona_addr).await?;
|
||||
let bob_fiona_contact_id = Contact::create(&bob, "Fiona", fiona_addr).await?;
|
||||
let alice_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent_msg = alice.send_text(alice_chat_id, "I created a group").await;
|
||||
let bob_received_msg = bob.recv_msg(&sent_msg).await;
|
||||
let bob_chat_id = bob_received_msg.get_chat_id();
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, bob_fiona_contact_id).await?;
|
||||
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Welcome, Fiona!").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
bob.golden_test_chat(bob_chat_id, "chat_test_msg_with_implicit_member_add")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_modify_chat_multi_device() -> Result<()> {
|
||||
let a1 = TestContext::new_alice().await;
|
||||
@@ -4693,7 +4925,7 @@ mod tests {
|
||||
|
||||
// adding or removing contacts from one-to-one-chats result in an error
|
||||
let claire = Contact::create(&ctx, "", "claire@foo.de").await.unwrap();
|
||||
let added = add_contact_to_chat_ex(&ctx, chat.id, claire, false).await;
|
||||
let added = add_contact_to_chat_ex(&ctx, Nosync, chat.id, claire, false).await;
|
||||
assert!(added.is_err());
|
||||
assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1);
|
||||
|
||||
@@ -5359,7 +5591,7 @@ mod tests {
|
||||
let (contact_id, _) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"",
|
||||
ContactAddress::new("foo@bar.org")?,
|
||||
&ContactAddress::new("foo@bar.org")?,
|
||||
Origin::IncomingUnknownTo,
|
||||
)
|
||||
.await?;
|
||||
@@ -6323,13 +6555,54 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_multidev() -> Result<()> {
|
||||
let alices = [
|
||||
TestContext::new_alice().await,
|
||||
TestContext::new_alice().await,
|
||||
];
|
||||
let bob = TestContext::new_bob().await;
|
||||
let a1b_contact_id = alices[1].add_or_lookup_contact(&bob).await.id;
|
||||
|
||||
let a0_broadcast_id = create_broadcast_list(&alices[0]).await?;
|
||||
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
|
||||
set_chat_name(&alices[0], a0_broadcast_id, "Broadcast list 42").await?;
|
||||
let sent_msg = alices[0].send_text(a0_broadcast_id, "hi").await;
|
||||
let msg = alices[1].recv_msg(&sent_msg).await;
|
||||
let a1_broadcast_id = get_chat_id_by_grpid(&alices[1], &a0_broadcast_chat.grpid)
|
||||
.await?
|
||||
.unwrap()
|
||||
.0;
|
||||
assert_eq!(msg.chat_id, a1_broadcast_id);
|
||||
let a1_broadcast_chat = Chat::load_from_db(&alices[1], a1_broadcast_id).await?;
|
||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
|
||||
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
|
||||
assert!(get_chat_contacts(&alices[1], a1_broadcast_id)
|
||||
.await?
|
||||
.is_empty());
|
||||
|
||||
add_contact_to_chat(&alices[1], a1_broadcast_id, a1b_contact_id).await?;
|
||||
set_chat_name(&alices[1], a1_broadcast_id, "Broadcast list 43").await?;
|
||||
let sent_msg = alices[1].send_text(a1_broadcast_id, "hi").await;
|
||||
let msg = alices[0].recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, a0_broadcast_id);
|
||||
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
|
||||
assert_eq!(a0_broadcast_chat.get_type(), Chattype::Broadcast);
|
||||
assert_eq!(a0_broadcast_chat.get_name(), "Broadcast list 42");
|
||||
assert!(get_chat_contacts(&alices[0], a0_broadcast_id)
|
||||
.await?
|
||||
.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_for_contact_with_blocked() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let (contact_id, _) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"",
|
||||
ContactAddress::new("foo@bar.org")?,
|
||||
&ContactAddress::new("foo@bar.org")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
@@ -6611,4 +6884,159 @@ mod tests {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_alter_chat() -> Result<()> {
|
||||
let alices = [
|
||||
TestContext::new_alice().await,
|
||||
TestContext::new_alice().await,
|
||||
];
|
||||
for a in &alices {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let ba_chat = bob.create_chat(&alices[0]).await;
|
||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||
let a0b_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
|
||||
alices[1].recv_msg(&sent_msg).await;
|
||||
let ab_contact_ids = [
|
||||
alices[0].add_or_lookup_contact(&bob).await.id,
|
||||
alices[1].add_or_lookup_contact(&bob).await.id,
|
||||
];
|
||||
|
||||
async fn sync(alices: &[TestContext]) -> Result<()> {
|
||||
let sync_msg = alices.get(0).unwrap().pop_sent_msg().await;
|
||||
alices.get(1).unwrap().recv_msg(&sync_msg).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Request);
|
||||
a0b_chat_id.accept(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
|
||||
a0b_chat_id.block(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Yes);
|
||||
a0b_chat_id.unblock(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
|
||||
|
||||
// Unblocking a 1:1 chat doesn't unblock the contact currently.
|
||||
Contact::unblock(&alices[0], ab_contact_ids[0]).await?;
|
||||
|
||||
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
||||
Contact::block(&alices[0], ab_contact_ids[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert!(alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
||||
Contact::unblock(&alices[0], ab_contact_ids[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
||||
|
||||
// Test accepting and blocking groups. This way we test:
|
||||
// - Group chats synchronisation.
|
||||
// - That blocking a group deletes it on other devices.
|
||||
let fiona = TestContext::new_fiona().await;
|
||||
let fiona_grp_chat_id = fiona
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&alices[0]])
|
||||
.await;
|
||||
let sent_msg = fiona.send_text(fiona_grp_chat_id, "hi").await;
|
||||
let a0_grp_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
|
||||
let a1_grp_chat_id = alices[1].recv_msg(&sent_msg).await.chat_id;
|
||||
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
|
||||
assert_eq!(a1_grp_chat.blocked, Blocked::Request);
|
||||
a0_grp_chat_id.accept(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
|
||||
assert_eq!(a1_grp_chat.blocked, Blocked::Not);
|
||||
a0_grp_chat_id.block(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert!(Chat::load_from_db(&alices[1], a1_grp_chat_id)
|
||||
.await
|
||||
.is_err());
|
||||
assert!(
|
||||
!alices[1]
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM chats WHERE id=?", (a1_grp_chat_id,))
|
||||
.await?
|
||||
);
|
||||
|
||||
// Test syncing of chat visibility on a self-chat. This way we test:
|
||||
// - Self-chat synchronisation.
|
||||
// - That sync messages don't unarchive the self-chat.
|
||||
let a0self_chat_id = alices[0].get_self_chat().await.id;
|
||||
assert_eq!(
|
||||
alices[1].get_self_chat().await.get_visibility(),
|
||||
ChatVisibility::Normal
|
||||
);
|
||||
let mut visibilities =
|
||||
ChatVisibility::iter().chain(std::iter::once(ChatVisibility::Normal));
|
||||
visibilities.next();
|
||||
for v in visibilities {
|
||||
a0self_chat_id.set_visibility(&alices[0], v).await?;
|
||||
sync(&alices).await?;
|
||||
for a in &alices {
|
||||
assert_eq!(a.get_self_chat().await.get_visibility(), v);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
alices[1].get_chat(&bob).await.mute_duration,
|
||||
MuteDuration::NotMuted
|
||||
);
|
||||
let mute_durations = [
|
||||
MuteDuration::Forever,
|
||||
MuteDuration::Until(SystemTime::now() + Duration::from_secs(42)),
|
||||
MuteDuration::NotMuted,
|
||||
];
|
||||
for m in mute_durations {
|
||||
set_muted(&alices[0], a0b_chat_id, m).await?;
|
||||
sync(&alices).await?;
|
||||
let m = match m {
|
||||
MuteDuration::Until(time) => MuteDuration::Until(
|
||||
SystemTime::UNIX_EPOCH
|
||||
+ Duration::from_secs(
|
||||
time.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
||||
),
|
||||
),
|
||||
_ => m,
|
||||
};
|
||||
assert_eq!(alices[1].get_chat(&bob).await.mute_duration, m);
|
||||
}
|
||||
|
||||
let a0_broadcast_id = create_broadcast_list(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
|
||||
set_chat_name(&alices[0], a0_broadcast_id, "Broadcast list 42").await?;
|
||||
sync(&alices).await?;
|
||||
let a1_broadcast_id = get_chat_id_by_grpid(&alices[1], &a0_broadcast_chat.grpid)
|
||||
.await?
|
||||
.unwrap()
|
||||
.0;
|
||||
let a1_broadcast_chat = Chat::load_from_db(&alices[1], a1_broadcast_id).await?;
|
||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
|
||||
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
|
||||
assert!(get_chat_contacts(&alices[1], a1_broadcast_id)
|
||||
.await?
|
||||
.is_empty());
|
||||
add_contact_to_chat(&alices[0], a0_broadcast_id, ab_contact_ids[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert_eq!(
|
||||
get_chat_contacts(&alices[1], a1_broadcast_id).await?,
|
||||
vec![ab_contact_ids[1]]
|
||||
);
|
||||
let sent_msg = alices[1].send_text(a1_broadcast_id, "hi").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||
assert_eq!(chat.get_type(), Chattype::Mailinglist);
|
||||
let msg = alices[0].recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, a0_broadcast_id);
|
||||
remove_contact_from_chat(&alices[0], a0_broadcast_id, ab_contact_ids[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert!(get_chat_contacts(&alices[1], a1_broadcast_id)
|
||||
.await?
|
||||
.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,6 +513,11 @@ impl Context {
|
||||
);
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
}
|
||||
Config::Addr => {
|
||||
self.sql
|
||||
.set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref())
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
}
|
||||
@@ -662,6 +667,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_config_addr() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Test that uppercase address get lowercased.
|
||||
assert!(t
|
||||
.set_config(Config::Addr, Some("Foobar@eXample.oRg"))
|
||||
.await
|
||||
.is_ok());
|
||||
assert_eq!(
|
||||
t.get_config(Config::Addr).await.unwrap().unwrap(),
|
||||
"foobar@example.org"
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests that "bot" config can only be set to "0" or "1".
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_config_bot() {
|
||||
|
||||
118
src/contact.rs
118
src/contact.rs
@@ -43,43 +43,43 @@ use crate::{chat, stock_str};
|
||||
const SEEN_RECENTLY_SECONDS: i64 = 600;
|
||||
|
||||
/// Valid contact address.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct ContactAddress<'a>(&'a str);
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ContactAddress(String);
|
||||
|
||||
impl Deref for ContactAddress<'_> {
|
||||
impl Deref for ContactAddress {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ContactAddress<'_> {
|
||||
impl AsRef<str> for ContactAddress {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContactAddress<'_> {
|
||||
impl fmt::Display for ContactAddress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ContactAddress<'a> {
|
||||
impl ContactAddress {
|
||||
/// Constructs a new contact address from string,
|
||||
/// normalizing and validating it.
|
||||
pub fn new(s: &'a str) -> Result<Self> {
|
||||
pub fn new(s: &str) -> Result<Self> {
|
||||
let addr = addr_normalize(s);
|
||||
if !may_be_valid_addr(addr) {
|
||||
if !may_be_valid_addr(&addr) {
|
||||
bail!("invalid address {:?}", s);
|
||||
}
|
||||
Ok(Self(addr))
|
||||
Ok(Self(addr.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow converting [`ContactAddress`] to an SQLite type.
|
||||
impl rusqlite::types::ToSql for ContactAddress<'_> {
|
||||
impl rusqlite::types::ToSql for ContactAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Text(self.0.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
@@ -149,6 +149,22 @@ impl ContactId {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reset gossip timestamp in all chats with this contact.
|
||||
pub(crate) async fn regossip_keys(&self, context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats
|
||||
SET gossiped_timestamp=0
|
||||
WHERE EXISTS (SELECT 1 FROM chats_contacts
|
||||
WHERE chats_contacts.chat_id=chats.id
|
||||
AND chats_contacts.contact_id=?)",
|
||||
(self,),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContactId {
|
||||
@@ -484,7 +500,7 @@ impl Contact {
|
||||
let addr = ContactAddress::new(&addr)?;
|
||||
|
||||
let (contact_id, sth_modified) =
|
||||
Contact::add_or_lookup(context, &name, addr, Origin::ManuallyCreated)
|
||||
Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated)
|
||||
.await
|
||||
.context("add_or_lookup")?;
|
||||
let blocked = Contact::is_blocked_load(context, contact_id).await?;
|
||||
@@ -549,7 +565,7 @@ impl Contact {
|
||||
|
||||
let addr_normalized = addr_normalize(addr);
|
||||
|
||||
if context.is_self_addr(addr_normalized).await? {
|
||||
if context.is_self_addr(&addr_normalized).await? {
|
||||
return Ok(Some(ContactId::SELF));
|
||||
}
|
||||
|
||||
@@ -599,7 +615,7 @@ impl Contact {
|
||||
pub(crate) async fn add_or_lookup(
|
||||
context: &Context,
|
||||
name: &str,
|
||||
addr: ContactAddress<'_>,
|
||||
addr: &ContactAddress,
|
||||
mut origin: Origin,
|
||||
) -> Result<(ContactId, Modifier)> {
|
||||
let mut sth_modified = Modifier::None;
|
||||
@@ -607,7 +623,7 @@ impl Contact {
|
||||
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
|
||||
ensure!(origin != Origin::Unknown, "Missing valid origin");
|
||||
|
||||
if context.is_self_addr(&addr).await? {
|
||||
if context.is_self_addr(addr).await? {
|
||||
return Ok((ContactId::SELF, sth_modified));
|
||||
}
|
||||
|
||||
@@ -762,7 +778,7 @@ impl Contact {
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
addr,
|
||||
&addr,
|
||||
origin,
|
||||
if update_authname {
|
||||
name.to_string()
|
||||
@@ -807,7 +823,7 @@ impl Contact {
|
||||
let name = normalize_name(&name);
|
||||
match ContactAddress::new(&addr) {
|
||||
Ok(addr) => {
|
||||
match Contact::add_or_lookup(context, &name, addr, Origin::AddressBook).await {
|
||||
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
|
||||
Ok((_, modified)) => {
|
||||
if modified != Modifier::None {
|
||||
modify_cnt += 1
|
||||
@@ -1301,7 +1317,7 @@ impl Contact {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if verifier_addr == self.addr {
|
||||
if addr_cmp(&verifier_addr, &self.addr) {
|
||||
// Contact is directly verified via QR code.
|
||||
return Ok(Some(ContactId::SELF));
|
||||
}
|
||||
@@ -1389,12 +1405,13 @@ pub fn may_be_valid_addr(addr: &str) -> bool {
|
||||
res.is_ok()
|
||||
}
|
||||
|
||||
/// Returns address with whitespace trimmed and `mailto:` prefix removed.
|
||||
pub fn addr_normalize(addr: &str) -> &str {
|
||||
let norm = addr.trim();
|
||||
/// Returns address lowercased,
|
||||
/// with whitespace trimmed and `mailto:` prefix removed.
|
||||
pub fn addr_normalize(addr: &str) -> String {
|
||||
let norm = addr.trim().to_lowercase();
|
||||
|
||||
if norm.starts_with("mailto:") {
|
||||
norm.get(7..).unwrap_or(norm)
|
||||
norm.get(7..).unwrap_or(&norm).to_string()
|
||||
} else {
|
||||
norm
|
||||
}
|
||||
@@ -1480,12 +1497,12 @@ WHERE type=? AND id IN (
|
||||
|
||||
if sync.into() {
|
||||
let action = match new_blocking {
|
||||
true => sync::ChatAction::Block,
|
||||
false => sync::ChatAction::Unblock,
|
||||
true => chat::SyncAction::Block,
|
||||
false => chat::SyncAction::Unblock,
|
||||
};
|
||||
context
|
||||
.add_sync_item(SyncData::AlterChat {
|
||||
id: sync::ChatId::ContactAddr(contact.addr.clone()),
|
||||
id: chat::SyncId::ContactAddr(contact.addr.clone()),
|
||||
action,
|
||||
})
|
||||
.await?;
|
||||
@@ -1649,8 +1666,8 @@ fn cat_fingerprint(
|
||||
|
||||
/// Compares two email addresses, normalizing them beforehand.
|
||||
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
|
||||
let norm1 = addr_normalize(addr1).to_lowercase();
|
||||
let norm2 = addr_normalize(addr2).to_lowercase();
|
||||
let norm1 = addr_normalize(addr1);
|
||||
let norm2 = addr_normalize(addr2);
|
||||
|
||||
norm1 == norm2
|
||||
}
|
||||
@@ -1848,10 +1865,7 @@ mod tests {
|
||||
fn test_normalize_addr() {
|
||||
assert_eq!(addr_normalize("mailto:john@doe.com"), "john@doe.com");
|
||||
assert_eq!(addr_normalize(" hello@world.com "), "hello@world.com");
|
||||
|
||||
// normalisation preserves case to allow user-defined spelling.
|
||||
// however, case is ignored on addr_cmp()
|
||||
assert_ne!(addr_normalize("John@Doe.com"), "john@doe.com");
|
||||
assert_eq!(addr_normalize("John@Doe.com"), "john@doe.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1877,7 +1891,7 @@ mod tests {
|
||||
let (id, _modified) = Contact::add_or_lookup(
|
||||
&context.ctx,
|
||||
"bob",
|
||||
ContactAddress::new("user@example.org")?,
|
||||
&ContactAddress::new("user@example.org")?,
|
||||
Origin::IncomingReplyTo,
|
||||
)
|
||||
.await?;
|
||||
@@ -1905,7 +1919,7 @@ mod tests {
|
||||
let (contact_bob_id, modified) = Contact::add_or_lookup(
|
||||
&context.ctx,
|
||||
"someone",
|
||||
ContactAddress::new("user@example.org")?,
|
||||
&ContactAddress::new("user@example.org")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
@@ -1970,7 +1984,7 @@ mod tests {
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"bla foo",
|
||||
ContactAddress::new("one@eins.org").unwrap(),
|
||||
&ContactAddress::new("one@eins.org").unwrap(),
|
||||
Origin::IncomingUnknownTo,
|
||||
)
|
||||
.await
|
||||
@@ -1989,7 +2003,7 @@ mod tests {
|
||||
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"Real one",
|
||||
ContactAddress::new(" one@eins.org ").unwrap(),
|
||||
&ContactAddress::new(" one@eins.org ").unwrap(),
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await
|
||||
@@ -2005,7 +2019,7 @@ mod tests {
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"",
|
||||
ContactAddress::new("three@drei.sam").unwrap(),
|
||||
&ContactAddress::new("three@drei.sam").unwrap(),
|
||||
Origin::IncomingUnknownTo,
|
||||
)
|
||||
.await
|
||||
@@ -2022,7 +2036,7 @@ mod tests {
|
||||
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"m. serious",
|
||||
ContactAddress::new("three@drei.sam").unwrap(),
|
||||
&ContactAddress::new("three@drei.sam").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
@@ -2037,7 +2051,7 @@ mod tests {
|
||||
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"schnucki",
|
||||
ContactAddress::new("three@drei.sam").unwrap(),
|
||||
&ContactAddress::new("three@drei.sam").unwrap(),
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await
|
||||
@@ -2053,7 +2067,7 @@ mod tests {
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"",
|
||||
ContactAddress::new("alice@w.de").unwrap(),
|
||||
&ContactAddress::new("alice@w.de").unwrap(),
|
||||
Origin::IncomingUnknownTo,
|
||||
)
|
||||
.await
|
||||
@@ -2195,7 +2209,7 @@ mod tests {
|
||||
let (contact_id, _) = Contact::add_or_lookup(
|
||||
&alice,
|
||||
"Bob",
|
||||
ContactAddress::new("bob@example.net")?,
|
||||
&ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
@@ -2274,7 +2288,7 @@ mod tests {
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"bob1",
|
||||
ContactAddress::new("bob@example.org").unwrap(),
|
||||
&ContactAddress::new("bob@example.org").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
@@ -2290,7 +2304,7 @@ mod tests {
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"bob2",
|
||||
ContactAddress::new("bob@example.org").unwrap(),
|
||||
&ContactAddress::new("bob@example.org").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
@@ -2316,7 +2330,7 @@ mod tests {
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"bob4",
|
||||
ContactAddress::new("bob@example.org").unwrap(),
|
||||
&ContactAddress::new("bob@example.org").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
@@ -2345,7 +2359,7 @@ mod tests {
|
||||
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"claire1",
|
||||
ContactAddress::new("claire@example.org").unwrap(),
|
||||
&ContactAddress::new("claire@example.org").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
@@ -2361,7 +2375,7 @@ mod tests {
|
||||
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"claire2",
|
||||
ContactAddress::new("claire@example.org").unwrap(),
|
||||
&ContactAddress::new("claire@example.org").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
@@ -2386,7 +2400,7 @@ mod tests {
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"Bob",
|
||||
ContactAddress::new("bob@example.org")?,
|
||||
&ContactAddress::new("bob@example.org")?,
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await?;
|
||||
@@ -2398,7 +2412,7 @@ mod tests {
|
||||
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"Not Bob",
|
||||
ContactAddress::new("bob@example.org")?,
|
||||
&ContactAddress::new("bob@example.org")?,
|
||||
Origin::IncomingUnknownTo,
|
||||
)
|
||||
.await?;
|
||||
@@ -2411,7 +2425,7 @@ mod tests {
|
||||
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"Bob",
|
||||
ContactAddress::new("bob@example.org")?,
|
||||
&ContactAddress::new("bob@example.org")?,
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await?;
|
||||
@@ -2440,7 +2454,7 @@ mod tests {
|
||||
Contact::add_or_lookup(
|
||||
&t,
|
||||
"dave2",
|
||||
ContactAddress::new("dave@example.org").unwrap(),
|
||||
&ContactAddress::new("dave@example.org").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
@@ -2561,7 +2575,7 @@ mod tests {
|
||||
let (contact_bob_id, _modified) = Contact::add_or_lookup(
|
||||
&alice,
|
||||
"Bob",
|
||||
ContactAddress::new("bob@example.net")?,
|
||||
&ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
@@ -2724,7 +2738,7 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
let (contact_id, _) = Contact::add_or_lookup(
|
||||
&alice,
|
||||
"Bob",
|
||||
ContactAddress::new("bob@example.net")?,
|
||||
&ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -413,7 +413,8 @@ impl Context {
|
||||
.is_some()
|
||||
{
|
||||
let mut lock = self.ratelimit.write().await;
|
||||
*lock = Ratelimit::new(Duration::new(40, 0), 6.0);
|
||||
// Allow at least 1 message every second + a burst of 3.
|
||||
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
|
||||
}
|
||||
}
|
||||
self.scheduler.start(self.clone()).await;
|
||||
|
||||
@@ -619,11 +619,11 @@ impl Imap {
|
||||
.inner
|
||||
.status(folder, "(UIDNEXT)")
|
||||
.await
|
||||
.context("STATUS (UIDNEXT) error for {folder:?}")?;
|
||||
.with_context(|| format!("STATUS (UIDNEXT) error for {folder:?}"))?;
|
||||
|
||||
status
|
||||
.uid_next
|
||||
.context("STATUS {folder} (UIDNEXT) did not return UIDNEXT")?
|
||||
.with_context(|| format!("STATUS {folder} (UIDNEXT) did not return UIDNEXT"))?
|
||||
};
|
||||
mailbox.uid_next = Some(new_uid_next);
|
||||
|
||||
@@ -2519,7 +2519,7 @@ async fn add_all_recipients_as_contacts(
|
||||
let (_, modified) = Contact::add_or_lookup(
|
||||
context,
|
||||
&display_name_normalized,
|
||||
recipient_addr,
|
||||
&recipient_addr,
|
||||
Origin::OutgoingTo,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -35,7 +35,7 @@ impl Session {
|
||||
let status = self
|
||||
.status(folder, "(UIDNEXT)")
|
||||
.await
|
||||
.context("STATUS (UIDNEXT) error for {folder:?}")?;
|
||||
.with_context(|| format!("STATUS (UIDNEXT) error for {folder:?}"))?;
|
||||
if let Some(uid_next) = status.uid_next {
|
||||
let expected_uid_next = get_uid_next(context, folder)
|
||||
.await
|
||||
|
||||
@@ -316,7 +316,15 @@ impl<'a> MimeFactory<'a> {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat } => {
|
||||
if chat.is_protected() {
|
||||
PeerstateVerifiedStatus::BidirectVerified
|
||||
if self.msg.get_info_type() == SystemMessage::SecurejoinMessage {
|
||||
// Securejoin messages are supposed to verify a key.
|
||||
// In order to do this, it is necessary that they can be sent
|
||||
// to a key that is not yet verified.
|
||||
// This has to work independently of whether the chat is protected right now.
|
||||
PeerstateVerifiedStatus::Unverified
|
||||
} else {
|
||||
PeerstateVerifiedStatus::BidirectVerified
|
||||
}
|
||||
} else {
|
||||
PeerstateVerifiedStatus::Unverified
|
||||
}
|
||||
@@ -596,9 +604,10 @@ impl<'a> MimeFactory<'a> {
|
||||
|
||||
if let Loaded::Message { chat } = &self.loaded {
|
||||
if chat.typ == Chattype::Broadcast {
|
||||
let encoded_chat_name = encode_words(&chat.name);
|
||||
headers.protected.push(Header::new(
|
||||
"List-ID".into(),
|
||||
format!("{} <{}>", chat.name, chat.grpid),
|
||||
format!("{encoded_chat_name} <{}>", chat.grpid),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1910,7 +1919,7 @@ mod tests {
|
||||
let contact_id = Contact::add_or_lookup(
|
||||
&t,
|
||||
"Dave",
|
||||
ContactAddress::new("dave@example.com").unwrap(),
|
||||
&ContactAddress::new("dave@example.com").unwrap(),
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -163,10 +163,10 @@ pub enum SystemMessage {
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged = 10,
|
||||
|
||||
/// Chat protection is enabled.
|
||||
/// "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
ChatProtectionEnabled = 11,
|
||||
|
||||
/// Chat protection is disabled.
|
||||
/// "%1$s sent a message from another device."
|
||||
ChatProtectionDisabled = 12,
|
||||
|
||||
/// Self-sent-message that contains only json used for multi-device-sync;
|
||||
|
||||
@@ -648,7 +648,7 @@ impl Peerstate {
|
||||
let (new_contact_id, _) = Contact::add_or_lookup(
|
||||
context,
|
||||
"",
|
||||
new_addr,
|
||||
&new_addr,
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await?;
|
||||
|
||||
13
src/qr.rs
13
src/qr.rs
@@ -365,9 +365,10 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
|
||||
if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
|
||||
let addr = ContactAddress::new(addr)?;
|
||||
let (contact_id, _) = Contact::add_or_lookup(context, &name, addr, Origin::UnhandledQrScan)
|
||||
.await
|
||||
.with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan)
|
||||
.await
|
||||
.with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
|
||||
|
||||
if let (Some(grpid), Some(grpname)) = (grpid, grpname) {
|
||||
if context
|
||||
@@ -432,7 +433,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
if let Some(peerstate) = peerstate {
|
||||
let peerstate_addr = ContactAddress::new(&peerstate.addr)?;
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(context, &name, peerstate_addr, Origin::UnhandledQrScan)
|
||||
Contact::add_or_lookup(context, &name, &peerstate_addr, Origin::UnhandledQrScan)
|
||||
.await
|
||||
.context("add_or_lookup")?;
|
||||
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request)
|
||||
@@ -777,7 +778,7 @@ impl Qr {
|
||||
) -> Result<Self> {
|
||||
let addr = ContactAddress::new(addr)?;
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(context, name, addr, Origin::UnhandledQrScan).await?;
|
||||
Contact::add_or_lookup(context, name, &addr, Origin::UnhandledQrScan).await?;
|
||||
Ok(Qr::Addr { contact_id, draft })
|
||||
}
|
||||
}
|
||||
@@ -788,7 +789,7 @@ fn normalize_address(addr: &str) -> Result<String> {
|
||||
let new_addr = percent_decode_str(addr).decode_utf8()?;
|
||||
let new_addr = addr_normalize(&new_addr);
|
||||
|
||||
ensure!(may_be_valid_addr(new_addr), "Bad e-mail address");
|
||||
ensure!(may_be_valid_addr(&new_addr), "Bad e-mail address");
|
||||
|
||||
Ok(new_addr.to_string())
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ Can we chat at 1pm pacific, today?"
|
||||
let bob_id = Contact::add_or_lookup(
|
||||
&alice,
|
||||
"",
|
||||
ContactAddress::new("bob@example.net")?,
|
||||
&ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
|
||||
use crate::contact::{
|
||||
may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin,
|
||||
addr_cmp, may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin,
|
||||
};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc_inner;
|
||||
@@ -227,8 +227,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
.and_then(|value| mailparse::dateparse(value).ok())
|
||||
.map_or(rcvd_timestamp, |value| min(value, rcvd_timestamp + 60));
|
||||
|
||||
let updated_verified_key_addr =
|
||||
update_verified_keys(context, &mut mime_parser, from_id).await?;
|
||||
update_verified_keys(context, &mut mime_parser, from_id).await?;
|
||||
|
||||
// Add parts
|
||||
let received_msg = add_parts(
|
||||
@@ -281,19 +280,12 @@ pub(crate) async fn receive_imf_inner(
|
||||
MsgId::new_unset()
|
||||
};
|
||||
|
||||
if let Some(addr) = updated_verified_key_addr {
|
||||
let msg = stock_str::contact_setup_changed(context, &addr).await;
|
||||
chat::add_info_msg(context, chat_id, &msg, received_msg.sort_timestamp).await?;
|
||||
}
|
||||
|
||||
save_locations(context, &mime_parser, chat_id, from_id, insert_msg_id).await?;
|
||||
|
||||
if let Some(ref sync_items) = mime_parser.sync_items {
|
||||
if from_id == ContactId::SELF {
|
||||
if mime_parser.was_encrypted() {
|
||||
if let Err(err) = context.execute_sync_items(sync_items).await {
|
||||
warn!(context, "receive_imf cannot execute sync items: {err:#}.");
|
||||
}
|
||||
context.execute_sync_items(sync_items).await;
|
||||
} else {
|
||||
warn!(context, "Sync items are not encrypted.");
|
||||
}
|
||||
@@ -465,7 +457,7 @@ async fn add_parts(
|
||||
let mut chat_id_blocked = Blocked::Not;
|
||||
|
||||
let mut better_msg = None;
|
||||
let mut group_changes_msgs = Vec::new();
|
||||
let mut group_changes_msgs = (Vec::new(), None);
|
||||
if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
|
||||
better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await);
|
||||
}
|
||||
@@ -562,6 +554,11 @@ async fn add_parts(
|
||||
markseen_on_imap_table(context, rfc724_mid).await.ok();
|
||||
}
|
||||
|
||||
if chat_id.is_none() && is_mdn {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
info!(context, "Message is an MDN (TRASH).",);
|
||||
}
|
||||
|
||||
if chat_id.is_none() {
|
||||
// try to assign to a chat based on In-Reply-To/References:
|
||||
|
||||
@@ -639,14 +636,9 @@ async fn add_parts(
|
||||
if let Some(group_chat_id) = chat_id {
|
||||
if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? {
|
||||
let chat = Chat::load_from_db(context, group_chat_id).await?;
|
||||
if chat.is_protected() {
|
||||
if chat.typ == Chattype::Single {
|
||||
// Just assign the message to the 1:1 chat with the actual sender instead.
|
||||
chat_id = None;
|
||||
} else {
|
||||
let s = stock_str::unknown_sender_for_chat(context).await;
|
||||
mime_parser.replace_msg_by_error(&s);
|
||||
}
|
||||
if chat.is_protected() && chat.typ == Chattype::Single {
|
||||
// Just assign the message to the 1:1 chat with the actual sender instead.
|
||||
chat_id = None;
|
||||
} else {
|
||||
// In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
|
||||
// to the sender's name, indicating to the user that he/she is not part of the group.
|
||||
@@ -654,6 +646,12 @@ async fn add_parts(
|
||||
let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
|
||||
for part in &mut mime_parser.parts {
|
||||
part.param.set(Param::OverrideSenderDisplayname, name);
|
||||
|
||||
if chat.is_protected() {
|
||||
// In protected chat, also mark the message with an error.
|
||||
let s = stock_str::unknown_sender_for_chat(context).await;
|
||||
part.error = Some(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -932,6 +930,22 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if chat_id.is_none() {
|
||||
// Check if the message belongs to a broadcast list.
|
||||
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
|
||||
let listid = mailinglist_header_listid(mailinglist_header)?;
|
||||
chat_id = Some(
|
||||
if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
|
||||
id
|
||||
} else {
|
||||
let name =
|
||||
compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
|
||||
chat::create_broadcast_list_ex(context, Nosync, listid, name).await?
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fetching_existing_messages && mime_parser.decrypting_failed {
|
||||
@@ -1124,9 +1138,29 @@ async fn add_parts(
|
||||
|
||||
let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
|
||||
|
||||
group_changes_msgs = group_changes_msgs.into_iter().rev().collect();
|
||||
let mut parts = mime_parser.parts.iter_mut().peekable();
|
||||
while let Some(part) = parts.peek() {
|
||||
if let Some(msg) = group_changes_msgs.1 {
|
||||
match &better_msg {
|
||||
None => better_msg = Some(msg),
|
||||
Some(_) => group_changes_msgs.0.push(msg),
|
||||
}
|
||||
}
|
||||
|
||||
for group_changes_msg in group_changes_msgs.0 {
|
||||
// Currently all additional group changes messages are "Member added".
|
||||
chat::add_info_msg_with_cmd(
|
||||
context,
|
||||
chat_id,
|
||||
&group_changes_msg,
|
||||
SystemMessage::MemberAddedToGroup,
|
||||
sort_timestamp,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
for part in &mime_parser.parts {
|
||||
if part.is_reaction {
|
||||
let reaction_str = simplify::remove_footers(part.msg.as_str());
|
||||
set_msg_reaction(
|
||||
@@ -1159,10 +1193,7 @@ async fn add_parts(
|
||||
}
|
||||
|
||||
let mut txt_raw = "".to_string();
|
||||
let group_changes_msg = group_changes_msgs.pop();
|
||||
let (msg, typ): (&str, Viewtype) = if let Some(msg) = &group_changes_msg {
|
||||
(msg, Viewtype::Text)
|
||||
} else if let Some(better_msg) = &better_msg {
|
||||
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
|
||||
(better_msg, Viewtype::Text)
|
||||
} else {
|
||||
(&part.msg, part.typ)
|
||||
@@ -1290,10 +1321,6 @@ RETURNING id
|
||||
|
||||
debug_assert!(!row_id.is_special());
|
||||
created_db_entries.push(row_id);
|
||||
|
||||
if group_changes_msg.is_none() || (group_changes_msgs.is_empty() && better_msg.is_none()) {
|
||||
parts.next();
|
||||
}
|
||||
}
|
||||
|
||||
// check all parts whether they contain a new logging webxdc
|
||||
@@ -1678,17 +1705,6 @@ async fn create_or_lookup_group(
|
||||
members.dedup();
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, &members).await?;
|
||||
|
||||
// once, we have protected-chats explained in UI, we can uncomment the following lines.
|
||||
// ("verified groups" did not add a message anyway)
|
||||
//
|
||||
//if create_protected == ProtectionStatus::Protected {
|
||||
// set from_id=0 as it is not clear that the sender of this random group message
|
||||
// actually really has enabled chat-protection at some point.
|
||||
//new_chat_id
|
||||
// .add_protection_msg(context, ProtectionStatus::Protected, false, 0)
|
||||
// .await?;
|
||||
//}
|
||||
|
||||
context.emit_event(EventType::ChatModified(new_chat_id));
|
||||
}
|
||||
|
||||
@@ -1722,24 +1738,25 @@ async fn apply_group_changes(
|
||||
from_id: ContactId,
|
||||
to_ids: &[ContactId],
|
||||
is_partial_download: bool,
|
||||
) -> Result<Vec<String>> {
|
||||
) -> Result<(Vec<String>, Option<String>)> {
|
||||
if chat_id.is_special() {
|
||||
// Do not apply group changes to the trash chat.
|
||||
return Ok(Vec::new());
|
||||
return Ok((Vec::new(), None));
|
||||
}
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ != Chattype::Group {
|
||||
return Ok(Vec::new());
|
||||
return Ok((Vec::new(), None));
|
||||
}
|
||||
|
||||
let mut send_event_chat_modified = false;
|
||||
let (mut removed_id, mut added_id) = (None, None);
|
||||
let mut better_msg = None;
|
||||
let mut group_changes_msgs = Vec::new();
|
||||
|
||||
// True if a Delta Chat client has explicitly added our current primary address.
|
||||
let self_added =
|
||||
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
|
||||
context.get_primary_self_addr().await? == *added_addr
|
||||
addr_cmp(&context.get_primary_self_addr().await?, added_addr)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
@@ -1791,7 +1808,12 @@ async fn apply_group_changes(
|
||||
|
||||
if !chat.is_protected() {
|
||||
chat_id
|
||||
.inner_set_protection(context, ProtectionStatus::Protected)
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
smeared_time(context),
|
||||
Some(from_id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
@@ -1799,11 +1821,11 @@ async fn apply_group_changes(
|
||||
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
|
||||
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
|
||||
|
||||
group_changes_msgs.push(if removed_id == Some(from_id) {
|
||||
stock_str::msg_group_left_local(context, from_id).await
|
||||
better_msg = if removed_id == Some(from_id) {
|
||||
Some(stock_str::msg_group_left_local(context, from_id).await)
|
||||
} else {
|
||||
stock_str::msg_del_member_local(context, removed_addr, from_id).await
|
||||
});
|
||||
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
|
||||
};
|
||||
|
||||
if removed_id.is_some() {
|
||||
if !allow_member_list_changes {
|
||||
@@ -1816,8 +1838,7 @@ async fn apply_group_changes(
|
||||
warn!(context, "Removed {removed_addr:?} has no contact id.")
|
||||
}
|
||||
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
|
||||
group_changes_msgs
|
||||
.push(stock_str::msg_add_member_local(context, added_addr, from_id).await);
|
||||
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
|
||||
|
||||
if allow_member_list_changes {
|
||||
if !recreate_member_list {
|
||||
@@ -1858,20 +1879,21 @@ async fn apply_group_changes(
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
|
||||
group_changes_msgs
|
||||
.push(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
|
||||
better_msg = Some(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
|
||||
}
|
||||
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
|
||||
if value == "group-avatar-changed" {
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
// this is just an explicit message containing the group-avatar,
|
||||
// apart from that, the group-avatar is send along with various other messages
|
||||
group_changes_msgs.push(match avatar_action {
|
||||
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
|
||||
AvatarAction::Change(_) => {
|
||||
stock_str::msg_grp_img_changed(context, from_id).await
|
||||
better_msg = match avatar_action {
|
||||
AvatarAction::Delete => {
|
||||
Some(stock_str::msg_grp_img_deleted(context, from_id).await)
|
||||
}
|
||||
});
|
||||
AvatarAction::Change(_) => {
|
||||
Some(stock_str::msg_grp_img_changed(context, from_id).await)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1929,21 +1951,7 @@ async fn apply_group_changes(
|
||||
}
|
||||
|
||||
if new_members != chat_contacts {
|
||||
let new_members_ref = &new_members;
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
transaction
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?", (chat_id,))?;
|
||||
for contact_id in new_members_ref {
|
||||
transaction.execute(
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
||||
(chat_id, contact_id),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
chat::update_chat_contacts_table(context, chat_id, &new_members).await?;
|
||||
chat_contacts = new_members;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
@@ -1983,11 +1991,22 @@ async fn apply_group_changes(
|
||||
if send_event_chat_modified {
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
Ok(group_changes_msgs)
|
||||
Ok((group_changes_msgs, better_msg))
|
||||
}
|
||||
|
||||
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
|
||||
|
||||
fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
|
||||
Ok(match LIST_ID_REGEX.captures(list_id_header) {
|
||||
Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
|
||||
None => list_id_header
|
||||
.trim()
|
||||
.trim_start_matches('<')
|
||||
.trim_end_matches('>'),
|
||||
}
|
||||
.to_string())
|
||||
}
|
||||
|
||||
/// Create or lookup a mailing list chat.
|
||||
///
|
||||
/// `list_id_header` contains the Id that must be used for the mailing list
|
||||
@@ -1997,21 +2016,13 @@ static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unw
|
||||
///
|
||||
/// `mime_parser` is the corresponding message
|
||||
/// and is used to figure out the mailing list name from different header fields.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn create_or_lookup_mailinglist(
|
||||
context: &Context,
|
||||
allow_creation: bool,
|
||||
list_id_header: &str,
|
||||
mime_parser: &MimeMessage,
|
||||
) -> Result<Option<(ChatId, Blocked)>> {
|
||||
let listid = match LIST_ID_REGEX.captures(list_id_header) {
|
||||
Some(cap) => cap[2].trim().to_string(),
|
||||
None => list_id_header
|
||||
.trim()
|
||||
.trim_start_matches('<')
|
||||
.trim_end_matches('>')
|
||||
.to_string(),
|
||||
};
|
||||
let listid = mailinglist_header_listid(list_id_header)?;
|
||||
|
||||
if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
|
||||
return Ok(Some((chat_id, blocked)));
|
||||
@@ -2166,7 +2177,7 @@ async fn apply_mailinglist_changes(
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let (contact_id, _) = Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
|
||||
let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
|
||||
let mut contact = Contact::get_by_id(context, contact_id).await?;
|
||||
if contact.param.get(Param::ListId) != Some(listid) {
|
||||
contact.param.set(Param::ListId, listid);
|
||||
@@ -2286,9 +2297,6 @@ enum VerifiedEncryption {
|
||||
/// Moves secondary verified key to primary verified key
|
||||
/// if the message is signed with a secondary verified key.
|
||||
/// Removes secondary verified key if the message is signed with primary key.
|
||||
///
|
||||
/// Returns address of the peerstate if the primary verified key was updated,
|
||||
/// the caller then needs to add "Setup changed" notification somewhere.
|
||||
async fn update_verified_keys(
|
||||
context: &Context,
|
||||
mimeparser: &mut MimeMessage,
|
||||
@@ -2336,10 +2344,11 @@ async fn update_verified_keys(
|
||||
peerstate.verified_key = peerstate.secondary_verified_key.take();
|
||||
peerstate.verified_key_fingerprint = peerstate.secondary_verified_key_fingerprint.take();
|
||||
peerstate.verifier = peerstate.secondary_verifier.take();
|
||||
peerstate.fingerprint_changed = true;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
|
||||
// Primary verified key changed.
|
||||
Ok(Some(peerstate.addr.clone()))
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
@@ -2411,6 +2420,16 @@ async fn has_verified_encryption(
|
||||
return Ok(Verified);
|
||||
}
|
||||
|
||||
mark_recipients_as_verified(context, from_id, to_ids, mimeparser).await?;
|
||||
Ok(Verified)
|
||||
}
|
||||
|
||||
async fn mark_recipients_as_verified(
|
||||
context: &Context,
|
||||
from_id: ContactId,
|
||||
to_ids: Vec<ContactId>,
|
||||
mimeparser: &MimeMessage,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
@@ -2453,6 +2472,17 @@ async fn has_verified_encryption(
|
||||
if let Some(fp) = peerstate.gossip_key_fingerprint.clone() {
|
||||
peerstate.set_verified(PeerstateKeyType::GossipKey, fp, verifier_addr)?;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
|
||||
if !is_verified {
|
||||
let (to_contact_id, _) = Contact::add_or_lookup(
|
||||
context,
|
||||
"",
|
||||
&ContactAddress::new(&to_addr)?,
|
||||
Origin::Hidden,
|
||||
)
|
||||
.await?;
|
||||
ChatId::set_protection_for_contact(context, to_contact_id).await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The contact already has a verified key.
|
||||
@@ -2463,7 +2493,8 @@ async fn has_verified_encryption(
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Verified)
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the last message referenced from `References` header if it is in the database.
|
||||
@@ -2584,7 +2615,7 @@ async fn add_or_lookup_contacts_by_address_list(
|
||||
async fn add_or_lookup_contact_by_addr(
|
||||
context: &Context,
|
||||
display_name: Option<&str>,
|
||||
addr: ContactAddress<'_>,
|
||||
addr: ContactAddress,
|
||||
origin: Origin,
|
||||
) -> Result<ContactId> {
|
||||
if context.is_self_addr(&addr).await? {
|
||||
@@ -2593,7 +2624,7 @@ async fn add_or_lookup_contact_by_addr(
|
||||
let display_name_normalized = display_name.map(normalize_name).unwrap_or_default();
|
||||
|
||||
let (contact_id, _modified) =
|
||||
Contact::add_or_lookup(context, &display_name_normalized, addr, origin).await?;
|
||||
Contact::add_or_lookup(context, &display_name_normalized, &addr, origin).await?;
|
||||
Ok(contact_id)
|
||||
}
|
||||
|
||||
|
||||
@@ -431,7 +431,7 @@ async fn test_escaped_recipients() {
|
||||
let carl_contact_id = Contact::add_or_lookup(
|
||||
&t,
|
||||
"Carl",
|
||||
ContactAddress::new("carl@host.tld").unwrap(),
|
||||
&ContactAddress::new("carl@host.tld").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
@@ -477,7 +477,7 @@ async fn test_cc_to_contact() {
|
||||
let carl_contact_id = Contact::add_or_lookup(
|
||||
&t,
|
||||
"garabage",
|
||||
ContactAddress::new("carl@host.tld").unwrap(),
|
||||
&ContactAddress::new("carl@host.tld").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
@@ -2003,7 +2003,7 @@ async fn test_duplicate_message() -> Result<()> {
|
||||
let bob_contact_id = Contact::add_or_lookup(
|
||||
&alice,
|
||||
"Bob",
|
||||
ContactAddress::new("bob@example.org").unwrap(),
|
||||
&ContactAddress::new("bob@example.org").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await?
|
||||
@@ -2060,7 +2060,7 @@ async fn test_ignore_footer_status_from_mailinglist() -> Result<()> {
|
||||
let bob_id = Contact::add_or_lookup(
|
||||
&t,
|
||||
"",
|
||||
ContactAddress::new("bob@example.net").unwrap(),
|
||||
&ContactAddress::new("bob@example.net").unwrap(),
|
||||
Origin::IncomingUnknownCc,
|
||||
)
|
||||
.await?
|
||||
@@ -2139,7 +2139,7 @@ async fn test_ignore_old_status_updates() -> Result<()> {
|
||||
let bob_id = Contact::add_or_lookup(
|
||||
&t,
|
||||
"",
|
||||
ContactAddress::new("bob@example.net")?,
|
||||
&ContactAddress::new("bob@example.net")?,
|
||||
Origin::AddressBook,
|
||||
)
|
||||
.await?
|
||||
@@ -2623,19 +2623,17 @@ async fn test_incoming_contact_request() -> Result<()> {
|
||||
let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?;
|
||||
assert!(chat.is_contact_request());
|
||||
|
||||
loop {
|
||||
let event = t
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::IncomingMsg { chat_id, msg_id } => {
|
||||
assert_eq!(msg.chat_id, chat_id);
|
||||
assert_eq!(msg.id, msg_id);
|
||||
return Ok(());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
let event = t
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::IncomingMsg { chat_id, msg_id } => {
|
||||
assert_eq!(msg.chat_id, chat_id);
|
||||
assert_eq!(msg.id, msg_id);
|
||||
Ok(())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use core::fmt;
|
||||
use std::cmp::min;
|
||||
use std::{iter::once, ops::Deref, sync::Arc};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::Result;
|
||||
use humansize::{format_size, BINARY};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@@ -299,6 +299,10 @@ impl Context {
|
||||
.yellow {
|
||||
background-color: #fdc625;
|
||||
}
|
||||
.not-started-error {
|
||||
font-size: 2em;
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>"#
|
||||
@@ -318,7 +322,8 @@ impl Context {
|
||||
sched.smtp.state.connectivity.clone(),
|
||||
),
|
||||
_ => {
|
||||
return Err(anyhow!("Not started"));
|
||||
ret += "<div class=\"not-started-error\">Error: IO Not Started</div><p>Please report this issue to the app developer.</p>\n</body></html>\n";
|
||||
return Ok(ret);
|
||||
}
|
||||
};
|
||||
drop(lock);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol).
|
||||
//! Verified contact protocol implementation as [specified by countermitm project](https://securejoin.readthedocs.io/en/latest/new.html#setup-contact-protocol).
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::param::Param;
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType};
|
||||
use crate::qr::check_qr;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::token;
|
||||
use crate::tools::time;
|
||||
|
||||
@@ -425,6 +426,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
contact_id.regossip_keys(context).await?;
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await?;
|
||||
info!(context, "Auth verified.",);
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
@@ -443,8 +445,14 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
match chat::get_chat_id_by_grpid(context, field_grpid).await? {
|
||||
Some((group_chat_id, _, _)) => {
|
||||
secure_connection_established(context, contact_id, group_chat_id).await?;
|
||||
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
|
||||
.await?;
|
||||
chat::add_contact_to_chat_ex(
|
||||
context,
|
||||
Nosync,
|
||||
group_chat_id,
|
||||
contact_id,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
None => bail!("Chat {} not found", &field_grpid),
|
||||
}
|
||||
@@ -609,6 +617,8 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
peerstate.set_verified(PeerstateKeyType::GossipKey, fingerprint, addr)?;
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await.unwrap_or_default();
|
||||
|
||||
ChatId::set_protection_for_contact(context, contact_id).await?;
|
||||
} else if let Some(fingerprint) =
|
||||
mime_message.get_header(HeaderDef::SecureJoinFingerprint)
|
||||
{
|
||||
@@ -675,9 +685,6 @@ async fn secure_connection_established(
|
||||
contact_id: ContactId,
|
||||
chat_id: ChatId,
|
||||
) -> Result<()> {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let msg = stock_str::contact_verified(context, &contact).await;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
if context
|
||||
.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?
|
||||
@@ -763,7 +770,7 @@ fn encrypted_and_signed(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat;
|
||||
use crate::chat::ProtectionStatus;
|
||||
use crate::chat::{remove_contact_from_chat, ProtectionStatus};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::ContactAddress;
|
||||
@@ -915,25 +922,10 @@ mod tests {
|
||||
// Check Alice got the verified message in her 1:1 chat.
|
||||
{
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let msg_ids: Vec<_> = chat::get_chat_msgs(&alice.ctx, chat.get_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
chat::ChatItem::Message { msg_id } => Some(msg_id),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(msg_ids.len(), 2);
|
||||
|
||||
let msg0 = Message::load_from_db(&alice.ctx, msg_ids[0]).await.unwrap();
|
||||
assert!(msg0.is_info());
|
||||
assert!(msg0.get_text().contains("bob@example.net verified"));
|
||||
|
||||
let msg1 = Message::load_from_db(&alice.ctx, msg_ids[1]).await.unwrap();
|
||||
assert!(msg1.is_info());
|
||||
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
|
||||
assert!(msg.is_info());
|
||||
let expected_text = chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg1.get_text(), expected_text);
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
}
|
||||
|
||||
// Check Alice sent the right message to Bob.
|
||||
@@ -969,24 +961,10 @@ mod tests {
|
||||
// Check Bob got the verified message in his 1:1 chat.
|
||||
{
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let msg_ids: Vec<_> = chat::get_chat_msgs(&bob.ctx, chat.get_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
chat::ChatItem::Message { msg_id } => Some(msg_id),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let msg0 = Message::load_from_db(&bob.ctx, msg_ids[0]).await.unwrap();
|
||||
assert!(msg0.is_info());
|
||||
assert!(msg0.get_text().contains("alice@example.org verified"));
|
||||
|
||||
let msg1 = Message::load_from_db(&bob.ctx, msg_ids[1]).await.unwrap();
|
||||
assert!(msg1.is_info());
|
||||
let msg = get_chat_msg(&bob, chat.get_id(), 0, 1).await;
|
||||
assert!(msg.is_info());
|
||||
let expected_text = chat_protection_enabled(&bob).await;
|
||||
assert_eq!(msg1.get_text(), expected_text);
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
}
|
||||
|
||||
// Check Bob sent the final message
|
||||
@@ -1080,7 +1058,7 @@ mod tests {
|
||||
let (contact_bob_id, _modified) = Contact::add_or_lookup(
|
||||
&alice.ctx,
|
||||
"Bob",
|
||||
ContactAddress::new("bob@example.net")?,
|
||||
&ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
@@ -1285,11 +1263,11 @@ mod tests {
|
||||
);
|
||||
// There should be 3 messages in the chat:
|
||||
// - The ChatProtectionEnabled message
|
||||
// - bob@example.net verified
|
||||
// - You added member bob@example.net
|
||||
let msg = get_chat_msg(&alice, alice_chatid, 1, 3).await;
|
||||
let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await;
|
||||
assert!(msg.is_info());
|
||||
assert!(msg.get_text().contains("bob@example.net verified"));
|
||||
let expected_text = chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
}
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
@@ -1325,27 +1303,6 @@ mod tests {
|
||||
println!("msg {msg_id} text: {text}");
|
||||
}
|
||||
}
|
||||
let mut msg_iter = chat::get_chat_msgs(&bob.ctx, bob_chatid)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter();
|
||||
loop {
|
||||
match msg_iter.next() {
|
||||
Some(chat::ChatItem::Message { msg_id }) => {
|
||||
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
|
||||
let text = msg.get_text();
|
||||
match text.contains("alice@example.org verified") {
|
||||
true => {
|
||||
assert!(msg.is_info());
|
||||
break;
|
||||
}
|
||||
false => continue,
|
||||
}
|
||||
}
|
||||
Some(_) => continue,
|
||||
None => panic!("Verified message not found in Bob's group chat"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
@@ -1395,4 +1352,32 @@ First thread."#;
|
||||
assert!(get_securejoin_qr(&alice, Some(chat_id)).await.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unknown_sender() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
tcm.execute_securejoin(&alice, &bob).await;
|
||||
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Protected, "Group with Bob", &[&bob])
|
||||
.await;
|
||||
|
||||
let sent = alice.send_text(alice_chat_id, "Hi!").await;
|
||||
let bob_chat_id = bob.recv_msg(&sent).await.chat_id;
|
||||
|
||||
let sent = bob.send_text(bob_chat_id, "Hi hi!").await;
|
||||
|
||||
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
|
||||
// The message from Bob is delivered late, Bob is already removed.
|
||||
let msg = alice.recv_msg(&sent).await;
|
||||
assert_eq!(msg.text, "Hi hi!");
|
||||
assert_eq!(msg.error.unwrap(), "Unknown sender for this chat.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,9 +222,7 @@ impl BobState {
|
||||
/// This creates an info message in the chat being joined.
|
||||
async fn notify_peer_verified(&self, context: &Context) -> Result<()> {
|
||||
let contact = Contact::get_by_id(context, self.invite().contact_id()).await?;
|
||||
let msg = stock_str::contact_verified(context, &contact).await;
|
||||
let chat_id = self.joining_chat_id(context).await?;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
|
||||
if context
|
||||
.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
|
||||
@@ -149,7 +149,7 @@ pub enum StockMessage {
|
||||
however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
|
||||
WelcomeMessage = 71,
|
||||
|
||||
#[strum(props(fallback = "Unknown sender for this chat. See 'info' for more details."))]
|
||||
#[strum(props(fallback = "Unknown sender for this chat."))]
|
||||
UnknownSenderForChat = 72,
|
||||
|
||||
#[strum(props(fallback = "Message from %1$s"))]
|
||||
@@ -824,6 +824,7 @@ pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &C
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s verified.`.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
|
||||
let addr = &contact.get_name_n_addr();
|
||||
translated(context, StockMessage::ContactVerified)
|
||||
@@ -929,7 +930,7 @@ pub(crate) async fn welcome_message(context: &Context) -> String {
|
||||
translated(context, StockMessage::WelcomeMessage).await
|
||||
}
|
||||
|
||||
/// Stock string: `Unknown sender for this chat. See 'info' for more details.`.
|
||||
/// Stock string: `Unknown sender for this chat.`.
|
||||
pub(crate) async fn unknown_sender_for_chat(context: &Context) -> String {
|
||||
translated(context, StockMessage::UnknownSenderForChat).await
|
||||
}
|
||||
|
||||
321
src/sync.rs
321
src/sync.rs
@@ -5,11 +5,12 @@ use lettre_email::mime::{self};
|
||||
use lettre_email::PartBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{self, Chat, ChatVisibility};
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::Blocked;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
@@ -41,34 +42,28 @@ pub(crate) struct QrTokenData {
|
||||
pub(crate) grpid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub(crate) enum ChatId {
|
||||
ContactAddr(String),
|
||||
Grpid(String),
|
||||
// NOTE: Ad-hoc groups lack an identifier that can be used across devices so
|
||||
// block/mute/etc. actions on them are not synchronized to other devices.
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub(crate) enum ChatAction {
|
||||
Block,
|
||||
Unblock,
|
||||
Accept,
|
||||
SetVisibility(ChatVisibility),
|
||||
SetMuted(chat::MuteDuration),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) enum SyncData {
|
||||
AddQrToken(QrTokenData),
|
||||
DeleteQrToken(QrTokenData),
|
||||
AlterChat { id: ChatId, action: ChatAction },
|
||||
AlterChat {
|
||||
id: chat::SyncId,
|
||||
action: chat::SyncAction,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum SyncDataOrUnknown {
|
||||
SyncData(SyncData),
|
||||
Unknown(serde_json::Value),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct SyncItem {
|
||||
timestamp: i64,
|
||||
data: SyncData,
|
||||
|
||||
data: SyncDataOrUnknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -76,6 +71,12 @@ pub(crate) struct SyncItems {
|
||||
items: Vec<SyncItem>,
|
||||
}
|
||||
|
||||
impl From<SyncData> for SyncDataOrUnknown {
|
||||
fn from(sync_data: SyncData) -> Self {
|
||||
Self::SyncData(sync_data)
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Adds an item to the list of items that should be synchronized to other devices.
|
||||
///
|
||||
@@ -92,7 +93,10 @@ impl Context {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let item = SyncItem { timestamp, data };
|
||||
let item = SyncItem {
|
||||
timestamp,
|
||||
data: data.into(),
|
||||
};
|
||||
let item = serde_json::to_string(&item)?;
|
||||
self.sql
|
||||
.execute("INSERT INTO multi_device_sync (item) VALUES(?);", (item,))
|
||||
@@ -104,7 +108,7 @@ impl Context {
|
||||
/// Adds most recent qr-code tokens for a given chat to the list of items to be synced.
|
||||
/// If device synchronization is disabled,
|
||||
/// no tokens exist or the chat is unpromoted, the function does nothing.
|
||||
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<chat::ChatId>) -> Result<()> {
|
||||
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<ChatId>) -> Result<()> {
|
||||
if !self.get_config_bool(Config::SyncMsgs).await? {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -155,7 +159,7 @@ impl Context {
|
||||
pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
|
||||
if let Some((json, ids)) = self.build_sync_json().await? {
|
||||
let chat_id =
|
||||
chat::ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
|
||||
ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
|
||||
.await?;
|
||||
let mut msg = Message {
|
||||
chat_id,
|
||||
@@ -248,41 +252,50 @@ impl Context {
|
||||
/// as otherwise we would add in a dead-loop between two devices
|
||||
/// sending message back and forth.
|
||||
///
|
||||
/// If an error is returned, the caller shall not try over.
|
||||
/// Therefore, errors should only be returned on database errors or so.
|
||||
/// If eg. just an item cannot be deleted,
|
||||
/// that should not hold off the other items to be executed.
|
||||
pub(crate) async fn execute_sync_items(&self, items: &SyncItems) -> Result<()> {
|
||||
/// If an error is returned, the caller shall not try over because some sync items could be
|
||||
/// already executed. Sync items are considered independent and executed in the given order but
|
||||
/// regardless of whether executing of the previous items succeeded.
|
||||
pub(crate) async fn execute_sync_items(&self, items: &SyncItems) {
|
||||
info!(self, "executing {} sync item(s)", items.items.len());
|
||||
for item in &items.items {
|
||||
match &item.data {
|
||||
AddQrToken(token) => {
|
||||
let chat_id = if let Some(grpid) = &token.grpid {
|
||||
if let Some((chat_id, _, _)) =
|
||||
chat::get_chat_id_by_grpid(self, grpid).await?
|
||||
{
|
||||
Some(chat_id)
|
||||
} else {
|
||||
warn!(
|
||||
self,
|
||||
"Ignoring token for nonexistent/deleted group '{}'.", grpid
|
||||
);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
token::save(self, Namespace::InviteNumber, chat_id, &token.invitenumber)
|
||||
.await?;
|
||||
token::save(self, Namespace::Auth, chat_id, &token.auth).await?;
|
||||
SyncDataOrUnknown::SyncData(data) => match data {
|
||||
AddQrToken(token) => self.add_qr_token(token).await,
|
||||
DeleteQrToken(token) => self.delete_qr_token(token).await,
|
||||
AlterChat { id, action } => self.sync_alter_chat(id, action).await,
|
||||
},
|
||||
SyncDataOrUnknown::Unknown(data) => {
|
||||
warn!(self, "Ignored unknown sync item: {data}.");
|
||||
Ok(())
|
||||
}
|
||||
DeleteQrToken(token) => {
|
||||
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
|
||||
token::delete(self, Namespace::Auth, &token.auth).await?;
|
||||
}
|
||||
AlterChat { id, action } => self.sync_alter_chat(id, action).await?,
|
||||
}
|
||||
.log_err(self)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_qr_token(&self, token: &QrTokenData) -> Result<()> {
|
||||
let chat_id = if let Some(grpid) = &token.grpid {
|
||||
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(self, grpid).await? {
|
||||
Some(chat_id)
|
||||
} else {
|
||||
warn!(
|
||||
self,
|
||||
"Ignoring token for nonexistent/deleted group '{}'.", grpid
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
token::save(self, Namespace::InviteNumber, chat_id, &token.invitenumber).await?;
|
||||
token::save(self, Namespace::Auth, chat_id, &token.auth).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_qr_token(&self, token: &QrTokenData) -> Result<()> {
|
||||
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
|
||||
token::delete(self, Namespace::Auth, &token.auth).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -292,10 +305,9 @@ mod tests {
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::bail;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{Chat, ProtectionStatus};
|
||||
use crate::chat::Chat;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::test_utils::TestContext;
|
||||
@@ -319,13 +331,13 @@ mod tests {
|
||||
|
||||
assert!(t.build_sync_json().await?.is_none());
|
||||
|
||||
// Having one test on `SyncData::AlterChat` is sufficient here as `ChatAction::SetMuted`
|
||||
// introduces enums inside items and `SystemTime`. Let's avoid in-depth testing of the
|
||||
// serialiser here which is an external crate.
|
||||
// Having one test on `SyncData::AlterChat` is sufficient here as
|
||||
// `chat::SyncAction::SetMuted` introduces enums inside items and `SystemTime`. Let's avoid
|
||||
// in-depth testing of the serialiser here which is an external crate.
|
||||
t.add_sync_item_with_timestamp(
|
||||
SyncData::AlterChat {
|
||||
id: ChatId::ContactAddr("bob@example.net".to_string()),
|
||||
action: ChatAction::SetMuted(chat::MuteDuration::Until(
|
||||
id: chat::SyncId::ContactAddr("bob@example.net".to_string()),
|
||||
action: chat::SyncAction::SetMuted(chat::MuteDuration::Until(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_millis(42999),
|
||||
)),
|
||||
},
|
||||
@@ -394,54 +406,41 @@ mod tests {
|
||||
|
||||
assert!(t.parse_sync_items(r#"{"badname":[]}"#.to_string()).is_err());
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"BadItem":{"invitenumber":"in","auth":"a","grpid":null}}}]}"#
|
||||
.to_string(),
|
||||
)
|
||||
.is_err());
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":123}}}]}"#.to_string(),
|
||||
)
|
||||
.is_err()); // `123` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":true}}}]}"#.to_string(),
|
||||
)
|
||||
.is_err()); // `true` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":[]}}}]}"#.to_string(),
|
||||
)
|
||||
.is_err()); // `[]` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":{}}}}]}"#.to_string(),
|
||||
)
|
||||
.is_err()); // `{}` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","grpid":null}}}]}"#.to_string(),
|
||||
)
|
||||
.is_err()); // missing field
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781318,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Burn"}}}]}"#.to_string(),
|
||||
)
|
||||
.is_err()); // Unknown enum value
|
||||
for bad_item_example in [
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"BadItem":{"invitenumber":"in","auth":"a","grpid":null}}}]}"#,
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":123}}}]}"#, // `123` is invalid for `String`
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":true}}}]}"#, // `true` is invalid for `String`
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":[]}}}]}"#, // `[]` is invalid for `String`
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":{}}}}]}"#, // `{}` is invalid for `String`
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","grpid":null}}}]}"#, // missing field
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Burn"}}}]}"#, // Unknown enum value
|
||||
] {
|
||||
let sync_items = t.parse_sync_items(bad_item_example.to_string()).unwrap();
|
||||
assert_eq!(sync_items.items.len(), 1);
|
||||
assert!(matches!(sync_items.items[0].timestamp, 1631781316));
|
||||
assert!(matches!(
|
||||
sync_items.items[0].data,
|
||||
SyncDataOrUnknown::Unknown(_)
|
||||
));
|
||||
}
|
||||
|
||||
// Test enums inside items and SystemTime
|
||||
let sync_items = t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781318,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":{"SetMuted":{"Until":{"secs_since_epoch":42,"nanos_since_epoch":999000000}}}}}}]}"#.to_string(),
|
||||
)?;
|
||||
assert_eq!(sync_items.items.len(), 1);
|
||||
let AlterChat { id, action } = &sync_items.items.get(0).unwrap().data else {
|
||||
let SyncDataOrUnknown::SyncData(AlterChat { id, action }) =
|
||||
&sync_items.items.get(0).unwrap().data
|
||||
else {
|
||||
bail!("bad item");
|
||||
};
|
||||
assert_eq!(*id, ChatId::ContactAddr("bob@example.net".to_string()));
|
||||
assert_eq!(
|
||||
*id,
|
||||
chat::SyncId::ContactAddr("bob@example.net".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
*action,
|
||||
ChatAction::SetMuted(chat::MuteDuration::Until(
|
||||
chat::SyncAction::SetMuted(chat::MuteDuration::Until(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_millis(42999)
|
||||
))
|
||||
);
|
||||
@@ -474,7 +473,9 @@ mod tests {
|
||||
)?;
|
||||
|
||||
assert_eq!(sync_items.items.len(), 1);
|
||||
if let AddQrToken(token) = &sync_items.items.get(0).unwrap().data {
|
||||
if let SyncDataOrUnknown::SyncData(AddQrToken(token)) =
|
||||
&sync_items.items.get(0).unwrap().data
|
||||
{
|
||||
assert_eq!(token.invitenumber, "in");
|
||||
assert_eq!(token.auth, "yip");
|
||||
assert_eq!(token.grpid, None);
|
||||
@@ -512,7 +513,7 @@ mod tests {
|
||||
.to_string(),
|
||||
)
|
||||
?;
|
||||
t.execute_sync_items(&sync_items).await?;
|
||||
t.execute_sync_items(&sync_items).await;
|
||||
|
||||
assert!(
|
||||
Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Unknown)
|
||||
@@ -546,7 +547,7 @@ mod tests {
|
||||
// check that the used self-talk is not visible to the user
|
||||
// but that creation will still work (in this case, the chat is empty)
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
||||
let chat_id = chat::ChatId::create_for_contact(&alice, ContactId::SELF).await?;
|
||||
let chat_id = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
|
||||
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
||||
assert!(chat.is_self_talk());
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
||||
@@ -569,124 +570,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_alter_chat() -> Result<()> {
|
||||
let alices = [
|
||||
TestContext::new_alice().await,
|
||||
TestContext::new_alice().await,
|
||||
];
|
||||
for a in &alices {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let ba_chat = bob.create_chat(&alices[0]).await;
|
||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||
let a0b_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
|
||||
alices[1].recv_msg(&sent_msg).await;
|
||||
|
||||
async fn sync(alices: &[TestContext]) -> Result<()> {
|
||||
let sync_msg = alices.get(0).unwrap().pop_sent_msg().await;
|
||||
alices.get(1).unwrap().recv_msg(&sync_msg).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Request);
|
||||
a0b_chat_id.accept(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
|
||||
a0b_chat_id.block(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Yes);
|
||||
a0b_chat_id.unblock(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
|
||||
|
||||
// Unblocking a 1:1 chat doesn't unblock the contact currently.
|
||||
let a0b_contact_id = alices[0].add_or_lookup_contact(&bob).await.id;
|
||||
Contact::unblock(&alices[0], a0b_contact_id).await?;
|
||||
|
||||
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
||||
Contact::block(&alices[0], a0b_contact_id).await?;
|
||||
sync(&alices).await?;
|
||||
assert!(alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
||||
Contact::unblock(&alices[0], a0b_contact_id).await?;
|
||||
sync(&alices).await?;
|
||||
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
||||
|
||||
// Test accepting and blocking groups. This way we test:
|
||||
// - Group chats synchronisation.
|
||||
// - That blocking a group deletes it on other devices.
|
||||
let fiona = TestContext::new_fiona().await;
|
||||
let fiona_grp_chat_id = fiona
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&alices[0]])
|
||||
.await;
|
||||
let sent_msg = fiona.send_text(fiona_grp_chat_id, "hi").await;
|
||||
let a0_grp_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
|
||||
let a1_grp_chat_id = alices[1].recv_msg(&sent_msg).await.chat_id;
|
||||
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
|
||||
assert_eq!(a1_grp_chat.blocked, Blocked::Request);
|
||||
a0_grp_chat_id.accept(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
|
||||
assert_eq!(a1_grp_chat.blocked, Blocked::Not);
|
||||
a0_grp_chat_id.block(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert!(Chat::load_from_db(&alices[1], a1_grp_chat_id)
|
||||
.await
|
||||
.is_err());
|
||||
assert!(
|
||||
!alices[1]
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM chats WHERE id=?", (a1_grp_chat_id,))
|
||||
.await?
|
||||
);
|
||||
|
||||
// Test syncing of chat visibility on a self-chat. This way we test:
|
||||
// - Self-chat synchronisation.
|
||||
// - That sync messages don't unarchive the self-chat.
|
||||
let a0self_chat_id = alices[0].get_self_chat().await.id;
|
||||
assert_eq!(
|
||||
alices[1].get_self_chat().await.get_visibility(),
|
||||
ChatVisibility::Normal
|
||||
);
|
||||
let mut visibilities =
|
||||
ChatVisibility::iter().chain(std::iter::once(ChatVisibility::Normal));
|
||||
visibilities.next();
|
||||
for v in visibilities {
|
||||
a0self_chat_id.set_visibility(&alices[0], v).await?;
|
||||
sync(&alices).await?;
|
||||
for a in &alices {
|
||||
assert_eq!(a.get_self_chat().await.get_visibility(), v);
|
||||
}
|
||||
}
|
||||
|
||||
use chat::MuteDuration;
|
||||
assert_eq!(
|
||||
alices[1].get_chat(&bob).await.mute_duration,
|
||||
MuteDuration::NotMuted
|
||||
);
|
||||
let mute_durations = [
|
||||
MuteDuration::Forever,
|
||||
MuteDuration::Until(SystemTime::now() + Duration::from_secs(42)),
|
||||
MuteDuration::NotMuted,
|
||||
];
|
||||
for m in mute_durations {
|
||||
chat::set_muted(&alices[0], a0b_chat_id, m).await?;
|
||||
sync(&alices).await?;
|
||||
let m = match m {
|
||||
MuteDuration::Until(time) => MuteDuration::Until(
|
||||
SystemTime::UNIX_EPOCH
|
||||
+ Duration::from_secs(
|
||||
time.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
||||
),
|
||||
),
|
||||
_ => m,
|
||||
};
|
||||
assert_eq!(alices[1].get_chat(&bob).await.mute_duration, m);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,7 +580,7 @@ impl TestContext {
|
||||
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
|
||||
// origin when creating this contact.
|
||||
let (contact_id, modified) =
|
||||
Contact::add_or_lookup(self, &name, addr, Origin::MailinglistAddress)
|
||||
Contact::add_or_lookup(self, &name, &addr, Origin::MailinglistAddress)
|
||||
.await
|
||||
.expect("add_or_lookup");
|
||||
match modified {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::chat::{Chat, ProtectionStatus};
|
||||
use crate::chat::ProtectionStatus;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_GCL_FOR_FORWARDING;
|
||||
@@ -126,24 +126,22 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
|
||||
VerifiedStatus::BidirectVerified
|
||||
);
|
||||
|
||||
// As soon as Alice creates a chat with Fiona, it should directly be protected
|
||||
// Alice should have a hidden protected chat with Fiona
|
||||
{
|
||||
let chat = alice.create_chat(&fiona).await;
|
||||
let chat = alice.get_chat(&fiona).await;
|
||||
assert!(chat.is_protected());
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
let msg = get_chat_msg(&alice, chat.id, 0, 1).await;
|
||||
let expected_text = stock_str::chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg.text, expected_text);
|
||||
}
|
||||
|
||||
// Fiona should also see the chat as protected
|
||||
// Fiona should have a hidden protected chat with Alice
|
||||
{
|
||||
let rcvd = tcm.send_recv(&alice, &fiona, "Hi Fiona").await;
|
||||
let alice_fiona_id = rcvd.chat_id;
|
||||
let chat = Chat::load_from_db(&fiona, alice_fiona_id).await?;
|
||||
let chat = fiona.get_chat(&alice).await;
|
||||
assert!(chat.is_protected());
|
||||
|
||||
let msg0 = get_chat_msg(&fiona, chat.id, 0, 2).await;
|
||||
let msg0 = get_chat_msg(&fiona, chat.id, 0, 1).await;
|
||||
let expected_text = stock_str::chat_protection_enabled(&fiona).await;
|
||||
assert_eq!(msg0.text, expected_text);
|
||||
}
|
||||
@@ -165,6 +163,15 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
|
||||
assert!(!chat.is_protected());
|
||||
assert!(chat.is_protection_broken());
|
||||
|
||||
let msg1 = get_chat_msg(&alice, chat.id, 0, 3).await;
|
||||
assert_eq!(msg1.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
||||
|
||||
let msg2 = get_chat_msg(&alice, chat.id, 1, 3).await;
|
||||
assert_eq!(msg2.get_info_type(), SystemMessage::ChatProtectionDisabled);
|
||||
|
||||
let msg2 = get_chat_msg(&alice, chat.id, 2, 3).await;
|
||||
assert_eq!(msg2.text, "I have a new device");
|
||||
|
||||
// After recreating the chat, it should still be unprotected
|
||||
chat.id.delete(&alice).await?;
|
||||
|
||||
@@ -705,6 +712,39 @@ async fn test_break_protection_then_verify_again() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for the following bug:
|
||||
///
|
||||
/// - Scan your chat partner's QR Code
|
||||
/// - They change devices
|
||||
/// - Scan their QR code again
|
||||
///
|
||||
/// -> The re-verification fails.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verify_then_verify_again() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob]).await;
|
||||
|
||||
mark_as_verified(&alice, &bob).await;
|
||||
mark_as_verified(&bob, &alice).await;
|
||||
|
||||
alice.create_chat(&bob).await;
|
||||
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
|
||||
|
||||
tcm.section("Bob reinstalls DC");
|
||||
drop(bob);
|
||||
let bob_new = tcm.unconfigured().await;
|
||||
enable_verified_oneonone_chats(&[&bob_new]).await;
|
||||
bob_new.configure_addr("bob@example.net").await;
|
||||
e2ee::ensure_secret_key_exists(&bob_new).await?;
|
||||
|
||||
tcm.execute_securejoin(&bob_new, &alice).await;
|
||||
assert_verified(&alice, &bob_new, ProtectionStatus::Protected).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test:
|
||||
/// - Verify a contact
|
||||
/// - The contact stops using DC and sends a message from a classical MUA instead
|
||||
|
||||
@@ -17,7 +17,7 @@ Seen status synchronization | IMAP CONDSTORE extension ([RFC 7162][])
|
||||
Client/server identification | IMAP ID extension ([RFC 2971][])
|
||||
Authorization | OAuth2 ([RFC 6749][])
|
||||
End-to-end encryption | [Autocrypt Level 1][], OpenPGP ([RFC 4880][]), Security Multiparts for MIME ([RFC 1847][]) and [“Mixed Up” Encryption repairing](https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html)
|
||||
Detect/prevent active attacks | [countermitm][] protocols
|
||||
Detect/prevent active attacks | [securejoin][] protocols
|
||||
Compare public keys | [openpgp4fpr][] URI Scheme
|
||||
Header encryption | [Protected Headers for Cryptographic E-mail](https://datatracker.ietf.org/doc/draft-autocrypt-lamps-protected-headers/)
|
||||
Configuration assistance | [Autoconfigure](https://web.archive.org/web/20210402044801/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover][]
|
||||
@@ -29,7 +29,7 @@ Return receipts | Message Disposition Notification (MDN, [RFC 8
|
||||
Locations | KML ([Open Geospatial Consortium](http://www.opengeospatial.org/standards/kml/), [Google Dev](https://developers.google.com/kml/))
|
||||
|
||||
[Autocrypt Level 1]: https://autocrypt.org/level1.html
|
||||
[countermitm]: https://countermitm.readthedocs.io/en/latest/
|
||||
[securejoin]: https://securejoin.readthedocs.io/en/latest/
|
||||
[openpgp4fpr]: https://metacode.biz/openpgp/openpgp4fpr
|
||||
[Autodiscover]: https://learn.microsoft.com/en-us/exchange/autodiscover-service-for-exchange-2013
|
||||
[XEP-0392]: https://xmpp.org/extensions/xep-0392.html
|
||||
|
||||
8
test-data/golden/chat_test_msg_with_implicit_member_add
Normal file
8
test-data/golden/chat_test_msg_with_implicit_member_add
Normal file
@@ -0,0 +1,8 @@
|
||||
Group#Chat#10: Group chat [3 member(s)]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: (Contact#Contact#11): I created a group [FRESH]
|
||||
Msg#11: (Contact#Contact#11): Member Fiona (fiona@example.net) added by alice@example.org. [FRESH][INFO]
|
||||
Msg#12: Me (Contact#Contact#Self): You removed member Fiona (fiona@example.net). [INFO] o
|
||||
Msg#13: info (Contact#Contact#Info): Member Fiona (fiona@example.net) added. [NOTICED][INFO]
|
||||
Msg#14: (Contact#Contact#11): Welcome, Fiona! [FRESH]
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -2,6 +2,6 @@ Group#Chat#10: Group chat [4 member(s)]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: (Contact#Contact#10): Hi! I created a group. [FRESH]
|
||||
Msg#11: Me (Contact#Contact#Self): You left the group. [INFO] o
|
||||
Msg#12: (Contact#Contact#10): Member claire@example.net added by alice@example.org. [FRESH][INFO]
|
||||
Msg#13: (Contact#Contact#10): Member Me (bob@example.net) added. [FRESH][INFO]
|
||||
Msg#12: info (Contact#Contact#Info): Member Me (bob@example.net) added. [NOTICED][INFO]
|
||||
Msg#13: (Contact#Contact#10): Member claire@example.net added by alice@example.org. [FRESH][INFO]
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user