mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 13:36:30 +03:00
Compare commits
21 Commits
v1.131.1
...
link2xt/im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
820171bf8f | ||
|
|
87dd33f66e | ||
|
|
7d8d13759a | ||
|
|
2b4f2a9171 | ||
|
|
8e869de350 | ||
|
|
b0ef082b2a | ||
|
|
bf8e74198d | ||
|
|
e77805471c | ||
|
|
45a8004b33 | ||
|
|
990f4dce9b | ||
|
|
0f36197c54 | ||
|
|
890a2bcc15 | ||
|
|
224355e83a | ||
|
|
c6ea4e389a | ||
|
|
678142b3fb | ||
|
|
ae6f83cd21 | ||
|
|
626b2be1fe | ||
|
|
ac5c789c75 | ||
|
|
ce2878f1e8 | ||
|
|
d4162899b4 | ||
|
|
e900d50e38 |
30
CHANGELOG.md
30
CHANGELOG.md
@@ -1,5 +1,33 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
@@ -3180,3 +3208,5 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[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
|
||||
|
||||
88
Cargo.lock
generated
88
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",
|
||||
@@ -931,9 +931,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124"
|
||||
checksum = "28f85c3514d2a6e64160359b45a3918c3b4178bcbf4ae5d03ab2d02e521c479a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
@@ -1087,7 +1087,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.131.1"
|
||||
version = "1.131.3"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1165,7 +1165,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.131.1"
|
||||
version = "1.131.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.1.0",
|
||||
@@ -1189,7 +1189,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.131.1"
|
||||
version = "1.131.3"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1204,7 +1204,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.131.1"
|
||||
version = "1.131.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1229,7 +1229,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.131.1"
|
||||
version = "1.131.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1583,7 +1583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914"
|
||||
dependencies = [
|
||||
"base16ct 0.2.0",
|
||||
"crypto-bigint 0.5.3",
|
||||
"crypto-bigint 0.5.4",
|
||||
"digest 0.10.7",
|
||||
"ff 0.13.0",
|
||||
"generic-array",
|
||||
@@ -1798,9 +1798,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,7 +1813,7 @@ 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",
|
||||
]
|
||||
|
||||
@@ -1899,9 +1899,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f69037fe1b785e84986b4f2cbcf647381876a00671d25ceef715d7812dd7e1dd"
|
||||
checksum = "53a56f0780318174bad1c127063fd0c5fdfb35398e3cd79ffaab931a6c79df80"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
@@ -2150,9 +2150,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 +2160,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -2173,12 +2173,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 +2189,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 +2295,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.10"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f95b9abcae896730d42b78e09c155ed4ddf82c07b4de772c64aee5b2d8b7c150"
|
||||
checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -2472,16 +2466,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 +2473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.2",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4074,9 +4058,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.21"
|
||||
version = "0.38.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
|
||||
checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"errno",
|
||||
@@ -4743,9 +4727,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",
|
||||
]
|
||||
@@ -5014,7 +4998,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",
|
||||
@@ -5094,9 +5078,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",
|
||||
@@ -5105,9 +5089,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",
|
||||
@@ -5694,18 +5678,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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.131.1"
|
||||
version = "1.131.3"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.70"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.131.1"
|
||||
version = "1.131.3"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -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.131.1"
|
||||
version = "1.131.3"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -53,5 +53,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.131.1"
|
||||
"version": "1.131.3"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.131.1"
|
||||
version = "1.131.3"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -60,7 +60,100 @@ 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]:
|
||||
while True:
|
||||
event = joiner.wait_for_event()
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
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")
|
||||
@@ -102,60 +195,38 @@ def test_verified_group_recovery(acfactory, rpc) -> None:
|
||||
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")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.131.1"
|
||||
version = "1.131.3"
|
||||
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.131.1"
|
||||
"version": "1.131.3"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
2023-11-13
|
||||
2023-11-15
|
||||
45
src/chat.rs
45
src/chat.rs
@@ -993,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)?;
|
||||
@@ -2439,10 +2440,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}");
|
||||
}
|
||||
@@ -3733,7 +3737,7 @@ async fn rename_ex(
|
||||
if !success {
|
||||
bail!("Failed to set name");
|
||||
}
|
||||
if sync.into() {
|
||||
if sync.into() && chat.name != new_name {
|
||||
let sync_name = sync_name.to_string();
|
||||
chat.sync(context, SyncAction::Rename(sync_name))
|
||||
.await
|
||||
@@ -4676,6 +4680,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;
|
||||
|
||||
@@ -296,6 +296,12 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
DisableIdle,
|
||||
|
||||
/// Whether to avoid using IMAP NOTIFY even if the server supports it.
|
||||
///
|
||||
/// This is a developer option for testing prefetch without NOTIFY.
|
||||
#[strum(props(default = "0"))]
|
||||
DisableNotify,
|
||||
|
||||
/// Defines the max. size (in bytes) of messages downloaded automatically.
|
||||
/// 0 = no limit.
|
||||
#[strum(props(default = "0"))]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -595,6 +595,7 @@ impl Context {
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await?;
|
||||
let sync_msgs = self.get_config_int(Config::SyncMsgs).await?;
|
||||
let disable_idle = self.get_config_bool(Config::DisableIdle).await?;
|
||||
let disable_notify = self.get_config_bool(Config::DisableNotify).await?;
|
||||
|
||||
let prv_key_cnt = self.sql.count("SELECT COUNT(*) FROM keypairs;", ()).await?;
|
||||
|
||||
@@ -708,6 +709,7 @@ impl Context {
|
||||
res.insert("bcc_self", bcc_self.to_string());
|
||||
res.insert("sync_msgs", sync_msgs.to_string());
|
||||
res.insert("disable_idle", disable_idle.to_string());
|
||||
res.insert("disable_notify", disable_notify.to_string());
|
||||
res.insert("private_key_count", prv_key_cnt.to_string());
|
||||
res.insert("public_key_count", pub_key_cnt.to_string());
|
||||
res.insert("fingerprint", fingerprint_str);
|
||||
|
||||
32
src/imap.rs
32
src/imap.rs
@@ -448,6 +448,22 @@ impl Imap {
|
||||
self.session = None;
|
||||
}
|
||||
|
||||
/// Tries to setup NOTIFY.
|
||||
pub async fn setup_notify(&mut self, context: &Context) -> Result<()> {
|
||||
let session = self
|
||||
.session
|
||||
.as_mut()
|
||||
.context("no IMAP connection established")?;
|
||||
if session.can_notify() && !session.notify_set {
|
||||
let cmd = format!("NOTIFY SET (selected (Messagenew {PREFETCH_FLAGS} messageexpunge))");
|
||||
session.run_command_and_check_ok(cmd).await?;
|
||||
info!(context, "Enabled NOTIFY");
|
||||
session.notify_set = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// FETCH-MOVE-DELETE iteration.
|
||||
///
|
||||
/// Prefetches headers and downloads new message from the folder, moves messages away from the
|
||||
@@ -619,11 +635,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);
|
||||
|
||||
@@ -720,7 +736,9 @@ impl Imap {
|
||||
.await
|
||||
.context("prefetch_existing_msgs")?
|
||||
} else {
|
||||
self.prefetch(old_uid_next).await.context("prefetch")?
|
||||
self.prefetch(context, old_uid_next)
|
||||
.await
|
||||
.context("prefetch")?
|
||||
};
|
||||
let read_cnt = msgs.len();
|
||||
|
||||
@@ -1329,7 +1347,13 @@ impl Imap {
|
||||
|
||||
/// Prefetch all messages greater than or equal to `uid_next`. Returns a list of fetch results
|
||||
/// in the order of ascending delivery time to the server (INTERNALDATE).
|
||||
async fn prefetch(&mut self, uid_next: u32) -> Result<Vec<(u32, async_imap::types::Fetch)>> {
|
||||
async fn prefetch(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
uid_next: u32,
|
||||
) -> Result<Vec<(u32, async_imap::types::Fetch)>> {
|
||||
self.setup_notify(context).await?;
|
||||
|
||||
let session = self
|
||||
.session
|
||||
.as_mut()
|
||||
|
||||
@@ -9,6 +9,10 @@ pub(crate) struct Capabilities {
|
||||
/// <https://tools.ietf.org/html/rfc2177>
|
||||
pub can_idle: bool,
|
||||
|
||||
/// True if the server has NOTIFY capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc5465>
|
||||
pub can_notify: bool,
|
||||
|
||||
/// True if the server has MOVE capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc6851>
|
||||
pub can_move: bool,
|
||||
|
||||
@@ -56,6 +56,7 @@ async fn determine_capabilities(
|
||||
};
|
||||
let capabilities = Capabilities {
|
||||
can_idle: caps.has_str("IDLE"),
|
||||
can_notify: caps.has_str("NOTIFY"),
|
||||
can_move: caps.has_str("MOVE"),
|
||||
can_check_quota: caps.has_str("QUOTA"),
|
||||
can_condstore: caps.has_str("CONDSTORE"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,6 +19,9 @@ pub(crate) struct Session {
|
||||
pub selected_mailbox: Option<Mailbox>,
|
||||
|
||||
pub selected_folder_needs_expunge: bool,
|
||||
|
||||
/// True if NOTIFY SET command was executed in this session.
|
||||
pub notify_set: bool,
|
||||
}
|
||||
|
||||
impl Deref for Session {
|
||||
@@ -46,6 +49,7 @@ impl Session {
|
||||
selected_folder: None,
|
||||
selected_mailbox: None,
|
||||
selected_folder_needs_expunge: false,
|
||||
notify_set: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +57,10 @@ impl Session {
|
||||
self.capabilities.can_idle
|
||||
}
|
||||
|
||||
pub fn can_notify(&self) -> bool {
|
||||
self.capabilities.can_notify
|
||||
}
|
||||
|
||||
pub fn can_move(&self) -> bool {
|
||||
self.capabilities.can_move
|
||||
}
|
||||
|
||||
@@ -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,11 +280,6 @@ 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 {
|
||||
@@ -463,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);
|
||||
}
|
||||
@@ -560,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:
|
||||
|
||||
@@ -656,8 +655,7 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
let new_better_msg;
|
||||
(group_changes_msgs, new_better_msg) = apply_group_changes(
|
||||
group_changes_msgs = apply_group_changes(
|
||||
context,
|
||||
mime_parser,
|
||||
sent_timestamp,
|
||||
@@ -667,7 +665,6 @@ async fn add_parts(
|
||||
is_partial_download.is_some(),
|
||||
)
|
||||
.await?;
|
||||
better_msg = better_msg.or(new_better_msg)
|
||||
}
|
||||
|
||||
if chat_id.is_none() {
|
||||
@@ -901,8 +898,7 @@ async fn add_parts(
|
||||
}
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
let new_better_msg;
|
||||
(group_changes_msgs, new_better_msg) = apply_group_changes(
|
||||
group_changes_msgs = apply_group_changes(
|
||||
context,
|
||||
mime_parser,
|
||||
sent_timestamp,
|
||||
@@ -912,7 +908,6 @@ async fn add_parts(
|
||||
is_partial_download.is_some(),
|
||||
)
|
||||
.await?;
|
||||
better_msg = better_msg.or(new_better_msg)
|
||||
}
|
||||
|
||||
if chat_id.is_none() && self_sent {
|
||||
@@ -1142,9 +1137,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(
|
||||
@@ -1177,10 +1192,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)
|
||||
@@ -1308,10 +1320,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() {
|
||||
parts.next();
|
||||
}
|
||||
}
|
||||
|
||||
// check all parts whether they contain a new logging webxdc
|
||||
@@ -2288,9 +2296,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,
|
||||
@@ -2338,10 +2343,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)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -426,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)));
|
||||
@@ -684,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?
|
||||
@@ -924,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.
|
||||
@@ -978,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
|
||||
@@ -1294,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
|
||||
@@ -1334,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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
93
src/sync.rs
93
src/sync.rs
@@ -52,10 +52,18 @@ pub(crate) enum SyncData {
|
||||
},
|
||||
}
|
||||
|
||||
#[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)]
|
||||
@@ -63,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.
|
||||
///
|
||||
@@ -79,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,))
|
||||
@@ -242,9 +259,15 @@ impl Context {
|
||||
info!(self, "executing {} sync item(s)", items.items.len());
|
||||
for item in &items.items {
|
||||
match &item.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::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(())
|
||||
}
|
||||
}
|
||||
.log_err(self)
|
||||
.ok();
|
||||
@@ -383,48 +406,32 @@ 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!(
|
||||
@@ -466,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);
|
||||
|
||||
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 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