mirror of
https://github.com/chatmail/core.git
synced 2026-04-14 03:57:19 +03:00
Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1767dc153 | ||
|
|
016fb2ceb2 | ||
|
|
5c571520a0 | ||
|
|
ddefd2cf09 | ||
|
|
30a3eeece8 | ||
|
|
5919388588 | ||
|
|
2cc738f481 | ||
|
|
babd405928 | ||
|
|
e885857875 | ||
|
|
a1d57a2645 | ||
|
|
a28aecd4d1 | ||
|
|
a3f1ff2827 | ||
|
|
c4d1a639b0 | ||
|
|
8732b7a55c | ||
|
|
1b9148f28e | ||
|
|
e0129c3b43 | ||
|
|
60d41022ea | ||
|
|
dd4f2ac671 | ||
|
|
eebb2a3b68 | ||
|
|
0d62069b67 | ||
|
|
56cf2e6596 | ||
|
|
59bd5481b9 | ||
|
|
6c8da526a0 | ||
|
|
13bc8b78d7 | ||
|
|
c7c68094d9 | ||
|
|
84f54b10dc | ||
|
|
cebc9e3e91 | ||
|
|
1379f8a055 | ||
|
|
53d049e5f5 | ||
|
|
4968f72dfb | ||
|
|
b24a0ed8fd | ||
|
|
c810347c7c | ||
|
|
cf8c4142c7 | ||
|
|
f71901b5f9 | ||
|
|
4419f9c4e7 | ||
|
|
4e5982b682 | ||
|
|
39e1510e64 | ||
|
|
64206160cc | ||
|
|
6376659348 | ||
|
|
43b2a4ad27 | ||
|
|
eaf06bb239 | ||
|
|
5e26b5bfdc | ||
|
|
60fbb6df5a | ||
|
|
3e60ee9d3e | ||
|
|
54e79409e6 | ||
|
|
31d113207b | ||
|
|
3a014477e7 | ||
|
|
90d8c8baf5 | ||
|
|
1dee17f980 | ||
|
|
6aeb21d3af | ||
|
|
4747ae2f1c | ||
|
|
7968f55191 | ||
|
|
33aa3556d2 | ||
|
|
b8b7563fca | ||
|
|
0d3f90770e | ||
|
|
51a4f0aa76 | ||
|
|
ebb89e20b4 | ||
|
|
8fc60e321b | ||
|
|
8ef6b6089f | ||
|
|
5b5b26122e | ||
|
|
300f5be4f3 | ||
|
|
c5d47ffcb0 | ||
|
|
3b7b8ea0f1 | ||
|
|
59739ee5c9 | ||
|
|
63207eb681 | ||
|
|
65f09c238b | ||
|
|
bb97d842df | ||
|
|
4dba5ab5f9 | ||
|
|
4c0e46fd44 | ||
|
|
ee3b40a59a | ||
|
|
e511b87955 | ||
|
|
53f51ad312 | ||
|
|
3878c4f041 | ||
|
|
499e4d3242 | ||
|
|
b5d0907090 | ||
|
|
6613fa67ee | ||
|
|
41ec380b55 | ||
|
|
7fb305e898 | ||
|
|
d4255a4979 | ||
|
|
b21dcd17b7 | ||
|
|
0caea85d16 | ||
|
|
42e0fb5eb9 | ||
|
|
6061d71492 | ||
|
|
dbd8814d2c | ||
|
|
a3562c5940 | ||
|
|
49b07c1c6a | ||
|
|
51d220f1e0 | ||
|
|
9f81a94d86 | ||
|
|
f6098fc931 | ||
|
|
6e3c2fc839 | ||
|
|
30e616f74f | ||
|
|
5e29cae81a |
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -76,14 +76,13 @@ jobs:
|
||||
rust: 1.54.0
|
||||
python: false # Python bindings compilation on Windows is not supported.
|
||||
|
||||
# Minimum Supported Rust Version = 1.48.0
|
||||
# This is the Debian "bullseye" release version of Rust.
|
||||
# Minimum Supported Rust Version = 1.51.0
|
||||
#
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.48.0
|
||||
rust: 1.51.0
|
||||
python: 3.7
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -151,4 +150,4 @@ jobs:
|
||||
DCC_RS_TARGET: debug
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e lint,doc,py3
|
||||
run: tox -e lint,mypy,doc,py3
|
||||
|
||||
76
CHANGELOG.md
76
CHANGELOG.md
@@ -1,12 +1,84 @@
|
||||
# Changelog
|
||||
|
||||
## 1.66.0
|
||||
|
||||
### API changes
|
||||
- `dc_contact_get_last_seen()` added #2823
|
||||
- python: `Contact.last_seen` added #2823
|
||||
- removed `DC_STR_NEWGROUPDRAFT`, we don't set draft after creating group anymore #2805
|
||||
|
||||
### Changes
|
||||
- python: add cutil.from_optional_dc_charpointer() #2824
|
||||
- refactorings #2807 #2822 #2825
|
||||
|
||||
|
||||
## 1.65.0
|
||||
|
||||
### Changes
|
||||
- python: add mypy support and some type hints #2809
|
||||
|
||||
### Fixes
|
||||
- do not disable ephemeral timer when downloading a message partially #2811
|
||||
- apply existing ephemeral timer also to partially downloaded messages;
|
||||
after full download, the ephemeral timer starts over #2811
|
||||
- replace user-visible error on verification failure with warning;
|
||||
the error is logged to the corresponding chat anyway #2808
|
||||
|
||||
|
||||
## 1.64.0
|
||||
|
||||
### Fixes
|
||||
- add 'waiting for being added to the group' only for group-joins,
|
||||
not for setup-contact #2797
|
||||
- prioritize In-Reply-To: and References: headers over group IDs when assigning
|
||||
messages to chats to fix incorrect assignment of Delta Chat replies to
|
||||
classic email threads #2795
|
||||
|
||||
|
||||
## 1.63.0
|
||||
|
||||
### API changes
|
||||
- `dc_get_last_error()` added #2788
|
||||
|
||||
### Changes
|
||||
- Optimize Autocrypt gossip #2743
|
||||
|
||||
### Fixes
|
||||
- fix permanently hiding of one-to-one chats after secure-join #2791
|
||||
|
||||
|
||||
## 1.62.0
|
||||
|
||||
### API Changes
|
||||
- `dc_join_securejoin()` now always returns immediately;
|
||||
the returned chat may not allow sending (`dc_chat_can_send()` returns false)
|
||||
which may change as usual on `DC_EVENT_CHAT_MODIFIED` #2508 #2767
|
||||
- introduce multi-device-sync-messages;
|
||||
as older cores display them as files in self-chat,
|
||||
they are currently only sent if config option `send_sync_msgs` is set #2669
|
||||
- add `DC_EVENT_SELFAVATAR_CHANGED` #2742
|
||||
|
||||
### Changes
|
||||
- use system DNS instead of google for MX queries #2780
|
||||
- improve error logging #2758
|
||||
- improve tests #2764 #2781
|
||||
- improve ci #2770
|
||||
- refactorings #2677 #2728 #2740 #2729 #2766 #2778
|
||||
|
||||
### Fixes
|
||||
- add Let's Encrypt certificate to core as it may be missing older devices #2752
|
||||
- prioritize certificate setting from user over the one from provider-db #2749
|
||||
- fix "QR process failed" error #2725
|
||||
- do not update quota in endless loop #2726
|
||||
|
||||
|
||||
## 1.61.0
|
||||
|
||||
### API Changes
|
||||
- download-on-demand added: `dc_msg_get_download_status()`, `dc_download_full_msg()`
|
||||
- download-on-demand added: `dc_msg_get_download_state()`, `dc_download_full_msg()`
|
||||
and `download_limit` config option #2631 #2696
|
||||
- `dc_create_broadcast_list()` and chat type `DC_CHAT_TYPE_BROADCAST` added #2707 #2722
|
||||
- allow ui-specific configs: `dc_set_ui_config()` and `dc_get_ui_config()` #2672
|
||||
- allow ui-specific configs using `ui.`-prefix in key (`dc_set_config(context, "ui.*", value)`) #2672
|
||||
- new strings from `DC_STR_PARTIAL_DOWNLOAD_MSG_BODY`
|
||||
to `DC_STR_PART_OF_TOTAL_USED` #2631 #2694 #2707 #2723
|
||||
- emit warnings and errors from account manager with account-id 0 #2712
|
||||
|
||||
251
Cargo.lock
generated
251
Cargo.lock
generated
@@ -10,9 +10,9 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.16.0"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd"
|
||||
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
@@ -114,9 +114,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.44"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
|
||||
checksum = "38d9ff5d688f1c13395289f67db01d4826b46dd694e7580accdc3e8430f2d98e"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
@@ -206,7 +206,7 @@ dependencies = [
|
||||
"httparse",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"pin-project 1.0.8",
|
||||
"pin-project",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -226,7 +226,7 @@ dependencies = [
|
||||
"nom 6.1.2",
|
||||
"ouroboros",
|
||||
"pin-utils",
|
||||
"stop-token",
|
||||
"stop-token 0.2.0",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
@@ -310,7 +310,7 @@ dependencies = [
|
||||
"hostname 0.1.5",
|
||||
"log",
|
||||
"nom 5.1.2",
|
||||
"pin-project 1.0.8",
|
||||
"pin-project",
|
||||
"pin-utils",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -363,16 +363,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-tar"
|
||||
version = "0.3.0"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb619eae01ab289095debb1ff7c02710d5124c20edde1b2eca926572a34c3998"
|
||||
checksum = "5c49359998a76e32ef6e870dbc079ebad8f1e53e8441c5dd39d27b44493fe331"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"filetime",
|
||||
"libc",
|
||||
"pin-project 0.4.28",
|
||||
"redox_syscall 0.1.57",
|
||||
"xattr",
|
||||
"pin-project",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -423,9 +422,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.61"
|
||||
version = "0.3.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01"
|
||||
checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
@@ -469,21 +468,6 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bitfield"
|
||||
version = "0.13.2"
|
||||
@@ -652,9 +636,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.70"
|
||||
version = "1.0.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
|
||||
checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
|
||||
|
||||
[[package]]
|
||||
name = "cfb-mode"
|
||||
@@ -1072,7 +1056,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.61.0"
|
||||
version = "1.66.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1100,8 +1084,6 @@ dependencies = [
|
||||
"hex",
|
||||
"humansize",
|
||||
"image",
|
||||
"indexmap",
|
||||
"itertools",
|
||||
"kamadak-exif",
|
||||
"lettre_email",
|
||||
"libc",
|
||||
@@ -1114,7 +1096,6 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pgp",
|
||||
"pretty_assertions",
|
||||
"pretty_env_logger",
|
||||
"proptest",
|
||||
"quick-xml",
|
||||
@@ -1131,7 +1112,7 @@ dependencies = [
|
||||
"sha-1",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"stop-token",
|
||||
"stop-token 0.6.1",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"surf",
|
||||
@@ -1152,7 +1133,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.61.0"
|
||||
version = "1.66.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
@@ -1201,12 +1182,6 @@ dependencies = [
|
||||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.9.0"
|
||||
@@ -1518,7 +1493,7 @@ checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall 0.2.10",
|
||||
"redox_syscall",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@@ -1734,9 +1709,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.25.0"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7"
|
||||
checksum = "81a03ce013ffccead76c11a15751231f777d9295b845cc1266ed4d34fcbd7977"
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
@@ -1959,16 +1934,6 @@ dependencies = [
|
||||
"nom 6.1.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
|
||||
dependencies = [
|
||||
"autocfg 1.0.1",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "infer"
|
||||
version = "0.2.3"
|
||||
@@ -2104,9 +2069,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.102"
|
||||
version = "0.2.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
|
||||
checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -2116,9 +2081,9 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.22.2"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d"
|
||||
checksum = "abd5850c449b40bacb498b2bbdfaff648b1b055630073ba8db499caf2d0ea9f2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
@@ -2417,9 +2382,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.26.2"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39f37e50073ccad23b6d09bcb5b263f4e76d3bb6038e4a3c08e52162ffa8abc2"
|
||||
checksum = "c821014c18301591b89b843809ef953af9e3df0496c232d5c0611b0a52aac363"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -2517,15 +2482,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "output_vt100"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packed_simd"
|
||||
version = "0.3.3"
|
||||
@@ -2561,7 +2517,7 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall 0.2.10",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"winapi",
|
||||
]
|
||||
@@ -2633,33 +2589,13 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "918192b5c59119d51e0cd221f4d49dde9112824ba717369e903c97d076083d0f"
|
||||
dependencies = [
|
||||
"pin-project-internal 0.4.28",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08"
|
||||
dependencies = [
|
||||
"pin-project-internal 1.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3be26700300be6d9d23264c73211d8190e755b6b5ca7a1b28230025511b52a5e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2687,9 +2623,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.19"
|
||||
version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
|
||||
checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
@@ -2761,18 +2697,6 @@ version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0cfe1b2403f172ba0f234e500906ee0a3e493fb81092dac23ebefe129301cc"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"ctor",
|
||||
"diff",
|
||||
"output_vt100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_env_logger"
|
||||
version = "0.4.0"
|
||||
@@ -2821,9 +2745,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.29"
|
||||
version = "1.0.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
|
||||
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
@@ -2834,7 +2758,6 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
@@ -2844,8 +2767,6 @@ dependencies = [
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_xorshift",
|
||||
"regex-syntax",
|
||||
"rusty-fork",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2871,9 +2792,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.9"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
|
||||
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -2897,9 +2818,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "r2d2_sqlite"
|
||||
version = "0.18.0"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d24607049214c5e42d3df53ac1d8a23c34cc6a5eefe3122acb2c72174719959"
|
||||
checksum = "54ca3c9468a76fc2ad724c486a59682fc362efeac7b18d1c012958bc19f34800"
|
||||
dependencies = [
|
||||
"r2d2",
|
||||
"rusqlite",
|
||||
@@ -3037,12 +2958,6 @@ dependencies = [
|
||||
"num_cpus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.10"
|
||||
@@ -3059,7 +2974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
|
||||
dependencies = [
|
||||
"getrandom 0.2.3",
|
||||
"redox_syscall 0.2.10",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3139,9 +3054,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.25.3"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57adcf67c8faaf96f3248c2a7b419a0dbc52ebe36ba83dd57fe83827c1ea4eb3"
|
||||
checksum = "8a82b0b91fad72160c56bf8da7a549b25d7c31109f52cc1437eac4c0ad2550a7"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fallible-iterator",
|
||||
@@ -3183,16 +3098,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusty-fork"
|
||||
version = "0.3.0"
|
||||
name = "rustversion"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"quick-error 1.2.3",
|
||||
"tempfile",
|
||||
"wait-timeout",
|
||||
]
|
||||
checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
|
||||
|
||||
[[package]]
|
||||
name = "rustyline"
|
||||
@@ -3350,9 +3259,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.68"
|
||||
version = "1.0.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
|
||||
checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -3361,9 +3270,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_qs"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a72808528a89fa9eca23bbb6a1eb92cb639b881357269b6510f11e50c0f8a9"
|
||||
checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
@@ -3590,6 +3499,18 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stop-token"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3829f0fc642e19ef8a1072ba8e33139a385f609ba56761df501c5272a9704918"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"cfg-if 1.0.0",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "str-buf"
|
||||
version = "1.0.5"
|
||||
@@ -3604,19 +3525,20 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.21.0"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2"
|
||||
checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.21.1"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
|
||||
checksum = "00ad150e9d51e33e8142984f577662c1324d49f3be45ed37bac8645fdcbe0fe5"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -3628,10 +3550,11 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||
|
||||
[[package]]
|
||||
name = "surf"
|
||||
version = "2.3.1"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f856d60bdb4679fc9ec516c34093484e963431b5016a8429f85a8e74b5ccaa"
|
||||
checksum = "718b1ae6b50351982dedff021db0def601677f2120938b070eadb10ba4038dd7"
|
||||
dependencies = [
|
||||
"async-native-tls",
|
||||
"async-std",
|
||||
"async-trait",
|
||||
"cfg-if 1.0.0",
|
||||
@@ -3649,9 +3572,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.77"
|
||||
version = "1.0.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0"
|
||||
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3685,7 +3608,7 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"rand 0.8.4",
|
||||
"redox_syscall 0.2.10",
|
||||
"redox_syscall",
|
||||
"remove_dir_all",
|
||||
"winapi",
|
||||
]
|
||||
@@ -3710,18 +3633,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.29"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88"
|
||||
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.29"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c"
|
||||
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3901,9 +3824,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.6"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
|
||||
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
@@ -3999,15 +3922,6 @@ version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "waker-fn"
|
||||
version = "1.1.0"
|
||||
@@ -4200,15 +4114,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.3.0"
|
||||
@@ -4220,9 +4125,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c1e130bebaeab2f23886bf9acbaca14b092408c452543c857f66399cd6dab1"
|
||||
checksum = "bdff2024a851a322b08f179173ae2ba620445aef1e838f0c196820eade4ae0c7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
20
Cargo.toml
20
Cargo.toml
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.61.0"
|
||||
version = "1.66.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
resolver = "2"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
@@ -21,7 +22,7 @@ async-native-tls = { version = "0.3" }
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", features = ["socks5"] }
|
||||
async-std-resolver = "0.20"
|
||||
async-std = { version = "1", features = ["unstable"] }
|
||||
async-tar = "0.3"
|
||||
async-tar = { version = "0.4", default-features=false }
|
||||
async-trait = "0.1"
|
||||
backtrace = "0.3"
|
||||
base64 = "0.13"
|
||||
@@ -35,8 +36,6 @@ escaper = "0.1"
|
||||
futures = "0.3"
|
||||
hex = "0.4.0"
|
||||
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
indexmap = "1.7"
|
||||
itertools = "0.10"
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
@@ -52,10 +51,10 @@ pgp = { version = "0.7", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
quick-xml = "0.22"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.18"
|
||||
r2d2_sqlite = "0.19"
|
||||
rand = "0.7"
|
||||
regex = "1.5"
|
||||
rusqlite = "0.25"
|
||||
rusqlite = "0.26"
|
||||
rust-hsluv = "0.1"
|
||||
rustyline = { version = "9.0", optional = true }
|
||||
sanitize-filename = "0.3"
|
||||
@@ -64,9 +63,9 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.9"
|
||||
sha2 = "0.9"
|
||||
smallvec = "1"
|
||||
stop-token = "0.2"
|
||||
strum = "0.21"
|
||||
strum_macros = "0.21"
|
||||
stop-token = "0.6"
|
||||
strum = "0.23"
|
||||
strum_macros = "0.23"
|
||||
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1"
|
||||
toml = "0.5"
|
||||
@@ -81,9 +80,8 @@ async-std = { version = "1", features = ["unstable", "attributes"] }
|
||||
criterion = "0.3"
|
||||
futures-lite = "1.12"
|
||||
log = "0.4"
|
||||
pretty_assertions = "1.0"
|
||||
pretty_env_logger = "0.4"
|
||||
proptest = "1"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
|
||||
[workspace]
|
||||
|
||||
BIN
assets/root-certificates/letsencrypt/isrgrootx1.der
Normal file
BIN
assets/root-certificates/letsencrypt/isrgrootx1.der
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.61.0"
|
||||
version = "1.66.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -2130,7 +2130,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_FPR_MISMATCH 220 // id=contact
|
||||
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
|
||||
#define DC_QR_ACCOUNT 250 // text1=domain
|
||||
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain
|
||||
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
|
||||
#define DC_QR_ADDR 320 // id=contact
|
||||
#define DC_QR_TEXT 330 // text1=text
|
||||
#define DC_QR_URL 332 // text1=URL
|
||||
@@ -2249,21 +2249,12 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
|
||||
* This function is typically called when dc_check_qr() returns
|
||||
* lot.state=DC_QR_ASK_VERIFYCONTACT or lot.state=DC_QR_ASK_VERIFYGROUP.
|
||||
*
|
||||
* Depending on the given QR code,
|
||||
* this function may takes some time and sends and receives several messages.
|
||||
* Therefore, you should call it always in a separate thread;
|
||||
* if you want to abort it, you should call dc_stop_ongoing_process().
|
||||
* The function returns immediately and the handshake runs in background,
|
||||
* sending and receiving several messages.
|
||||
* During the handshake, info messages are added to the chat,
|
||||
* showing progress, success or errors.
|
||||
*
|
||||
* - If the given QR code starts the Setup-Contact protocol,
|
||||
* the function typically returns immediately
|
||||
* and the handshake runs in background.
|
||||
* Subsequent calls of dc_join_securejoin() will abort unfinished tasks.
|
||||
* The returned chat is the one-to-one opportunistic chat.
|
||||
* When the protocol has finished, an info-message is added to that chat.
|
||||
* - If the given QR code starts the Verified-Group-Invite protocol,
|
||||
* the function waits until the protocol has finished.
|
||||
* This is because the protected group is not opportunistic
|
||||
* and can be created only when the contacts have verified each other.
|
||||
* Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes.
|
||||
*
|
||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
@@ -2273,10 +2264,8 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
|
||||
* @param qr The text of the scanned QR code. Typically, the same string as given
|
||||
* to dc_check_qr().
|
||||
* @return Chat-id of the joined chat, the UI may redirect to the this chat.
|
||||
* If the out-of-band verification failed or was aborted, 0 is returned.
|
||||
* On errors, 0 is returned, however, most errors will happen during handshake later on.
|
||||
* A returned chat-id does not guarantee that the chat is protected or the belonging contact is verified.
|
||||
* If needed, this be checked with dc_chat_is_protected() and dc_contact_is_verified(),
|
||||
* however, in practise, the UI will just listen to #DC_EVENT_CONTACTS_CHANGED unconditionally.
|
||||
*/
|
||||
uint32_t dc_join_securejoin (dc_context_t* context, const char* qr);
|
||||
|
||||
@@ -2425,6 +2414,22 @@ dc_array_t* dc_get_locations (dc_context_t* context, uint32_t cha
|
||||
void dc_delete_all_locations (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Get last error string.
|
||||
*
|
||||
* This is the same error string as logged via #DC_EVENT_ERROR,
|
||||
* however, using this function avoids race conditions
|
||||
* if the failing function is called in another thread than dc_get_next_event().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @return Last error or an empty string if there is no last error.
|
||||
* NULL is never returned.
|
||||
* The returned value must be released using dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_get_last_error (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Release a string returned by another deltachat-core function.
|
||||
* - Strings returned by any deltachat-core-function
|
||||
@@ -4377,6 +4382,16 @@ uint32_t dc_contact_get_color (const dc_contact_t* contact);
|
||||
*/
|
||||
char* dc_contact_get_status (const dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Get the contact's last seen timestamp.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return Last seen timestamp.
|
||||
* 0 on error or if the contact was never seen.
|
||||
*/
|
||||
int64_t dc_contact_get_last_seen (const dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Check if a contact is blocked.
|
||||
*
|
||||
@@ -5368,6 +5383,14 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_CONNECTIVITY_CHANGED 2100
|
||||
|
||||
|
||||
/**
|
||||
* The user's avatar changed.
|
||||
* You can get the new avatar file with `dc_get_config(context, "selfavatar")`.
|
||||
*/
|
||||
#define DC_EVENT_SELFAVATAR_CHANGED 2110
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@@ -5600,12 +5623,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// if nothing else is set by the dc_set_config()-option `selfstatus`.
|
||||
#define DC_STR_STATUSLINE 13
|
||||
|
||||
/// "Hi, i've created the group %1$s for us."
|
||||
///
|
||||
/// Used as a draft text after group creation.
|
||||
/// - %1$s will be replaced by the group name
|
||||
#define DC_STR_NEWGROUPDRAFT 14
|
||||
|
||||
/// "Group name changed from %1$s to %2$s."
|
||||
///
|
||||
/// Used in status messages for group name changes.
|
||||
@@ -5951,6 +5968,21 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%1$s` will be replaced by human-readable date and time.
|
||||
#define DC_STR_DOWNLOAD_AVAILABILITY 100
|
||||
|
||||
/// "Multi Device Synchronization"
|
||||
///
|
||||
/// Used in subjects of outgoing sync messages.
|
||||
#define DC_STR_SYNC_MSG_SUBJECT 101
|
||||
|
||||
/// "This message is used to synchronize data between your devices."
|
||||
///
|
||||
///
|
||||
/// Used as message text of outgoing sync messages.
|
||||
/// The text is visible in non-dc-muas or in outdated Delta Chat versions,
|
||||
/// the default text therefore adds the following hint:
|
||||
/// "If you see this message in Delta Chat,
|
||||
/// please update your Delta Chat apps on all devices."
|
||||
#define DC_STR_SYNC_MSG_BODY 102
|
||||
|
||||
/// "Incoming Messages"
|
||||
///
|
||||
/// Used as a headline in the connectivity view.
|
||||
@@ -6025,6 +6057,26 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used for describing resource usage, resulting string will be eg. "1.2 GiB of 3 GiB used".
|
||||
#define DC_STR_PART_OF_TOTAL_USED 116
|
||||
|
||||
/// "%1$s invited you to join this group. Waiting for the device of %2$s to reply…"
|
||||
///
|
||||
/// Added as an info-message directly after scanning a QR code for joining a group.
|
||||
/// May be followed by the info-messages
|
||||
/// #DC_STR_SECURE_JOIN_REPLIES, #DC_STR_CONTACT_VERIFIED and #DC_STR_MSGADDMEMBER.
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the inviter,
|
||||
/// `%2$s` will be replaced by the name of the inviter.
|
||||
#define DC_STR_SECURE_JOIN_STARTED 117
|
||||
|
||||
/// "%1$s replied, waiting for being added to the group…"
|
||||
///
|
||||
/// Info-message on scanning a QR code for joining a group.
|
||||
/// Added after #DC_STR_SECURE_JOIN_STARTED.
|
||||
/// If the handshake allows to skip a step and go for #DC_STR_CONTACT_VERIFIED directly,
|
||||
/// this info-message is skipped.
|
||||
///
|
||||
/// `%1$s` will be replaced by the name of the inviter.
|
||||
#define DC_STR_SECURE_JOIN_REPLIES 118
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,7 @@ extern crate num_traits;
|
||||
extern crate serde_json;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryInto;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::Write;
|
||||
use std::ops::Deref;
|
||||
use std::ptr;
|
||||
@@ -39,6 +39,7 @@ use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
|
||||
mod dc_array;
|
||||
mod lot;
|
||||
|
||||
mod string;
|
||||
use self::string::*;
|
||||
@@ -437,6 +438,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::Warning(_)
|
||||
| EventType::Error(_)
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::ErrorSelfNotInGroup(_) => 0,
|
||||
EventType::MsgsChanged { chat_id, .. }
|
||||
| EventType::IncomingMsg { chat_id, .. }
|
||||
@@ -487,6 +489,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ImexFileWritten(_)
|
||||
| EventType::MsgsNoticed(_)
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::ChatModified(_) => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::IncomingMsg { msg_id, .. }
|
||||
@@ -536,6 +539,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::SecurejoinInviterProgress { .. }
|
||||
| EventType::SecurejoinJoinerProgress { .. }
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
@@ -2043,10 +2047,11 @@ pub unsafe extern "C" fn dc_check_qr(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
let lot = qr::check_qr(ctx, &to_string_lossy(qr)).await;
|
||||
Box::into_raw(Box::new(lot))
|
||||
})
|
||||
let lot = match block_on(qr::check_qr(ctx, &to_string_lossy(qr))) {
|
||||
Ok(qr) => qr.into(),
|
||||
Err(err) => err.into(),
|
||||
};
|
||||
Box::into_raw(Box::new(lot))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2201,6 +2206,16 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_last_error(context: *mut dc_context_t) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_last_error()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(ctx.get_last_error()).strdup()
|
||||
}
|
||||
|
||||
// dc_array_t
|
||||
|
||||
pub type dc_array_t = dc_array::dc_array_t;
|
||||
@@ -3047,7 +3062,7 @@ pub unsafe extern "C" fn dc_msg_get_summary(
|
||||
let ffi_msg = &mut *msg;
|
||||
let ctx = &*ffi_msg.context;
|
||||
|
||||
let summary = block_on(async move { ffi_msg.message.get_summary(ctx, maybe_chat).await })
|
||||
let summary = block_on(ffi_msg.message.get_summary(ctx, maybe_chat))
|
||||
.log_err(ctx, "dc_msg_get_summary failed")
|
||||
.unwrap_or_default();
|
||||
Box::into_raw(Box::new(summary.into()))
|
||||
@@ -3065,12 +3080,13 @@ pub unsafe extern "C" fn dc_msg_get_summarytext(
|
||||
let ffi_msg = &mut *msg;
|
||||
let ctx = &*ffi_msg.context;
|
||||
|
||||
block_on({
|
||||
ffi_msg
|
||||
.message
|
||||
.get_summarytext(ctx, approx_characters.try_into().unwrap_or_default())
|
||||
})
|
||||
.strdup()
|
||||
let summary = block_on(ffi_msg.message.get_summary(ctx, None))
|
||||
.log_err(ctx, "dc_msg_get_summarytext failed")
|
||||
.unwrap_or_default();
|
||||
match usize::try_from(approx_characters) {
|
||||
Ok(chars) => summary.truncated_text(chars).strdup(),
|
||||
Err(_) => summary.text.strdup(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3528,6 +3544,16 @@ pub unsafe extern "C" fn dc_contact_get_status(contact: *mut dc_contact_t) -> *m
|
||||
ffi_contact.contact.get_status().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_get_last_seen(contact: *mut dc_contact_t) -> i64 {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_last_seen()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.last_seen()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_is_blocked(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
@@ -3607,7 +3633,7 @@ pub unsafe extern "C" fn dc_lot_get_state(lot: *mut dc_lot_t) -> libc::c_int {
|
||||
}
|
||||
|
||||
let lot = &*lot;
|
||||
lot.get_state().to_i64().expect("impossible") as libc::c_int
|
||||
lot.get_state() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
245
deltachat-ffi/src/lot.rs
Normal file
245
deltachat-ffi/src/lot.rs
Normal file
@@ -0,0 +1,245 @@
|
||||
//! # Legacy generic return values for C API.
|
||||
|
||||
use crate::message::MessageState;
|
||||
use crate::qr::Qr;
|
||||
use crate::summary::{Summary, SummaryPrefix};
|
||||
use anyhow::Error;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// An object containing a set of values.
|
||||
/// The meaning of the values is defined by the function returning the object.
|
||||
/// Lot objects are created
|
||||
/// eg. by chatlist.get_summary() or dc_msg_get_summary().
|
||||
///
|
||||
/// *Lot* is used in the meaning *heap* here.
|
||||
#[derive(Debug)]
|
||||
pub enum Lot {
|
||||
Summary(Summary),
|
||||
Qr(Qr),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Meaning {
|
||||
None = 0,
|
||||
Text1Draft = 1,
|
||||
Text1Username = 2,
|
||||
Text1Self = 3,
|
||||
}
|
||||
|
||||
impl Default for Meaning {
|
||||
fn default() -> Self {
|
||||
Meaning::None
|
||||
}
|
||||
}
|
||||
|
||||
impl Lot {
|
||||
pub fn get_text1(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Summary(summary) => match &summary.prefix {
|
||||
None => None,
|
||||
Some(SummaryPrefix::Draft(text)) => Some(text),
|
||||
Some(SummaryPrefix::Username(username)) => Some(username),
|
||||
Some(SummaryPrefix::Me(text)) => Some(text),
|
||||
},
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { .. } => None,
|
||||
Qr::AskVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::FprOk { .. } => None,
|
||||
Qr::FprMismatch { .. } => None,
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||
Qr::Account { domain } => Some(domain),
|
||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||
Qr::Addr { .. } => None,
|
||||
Qr::Url { url } => Some(url),
|
||||
Qr::Text { text } => Some(text),
|
||||
Qr::WithdrawVerifyContact { .. } => None,
|
||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::ReviveVerifyContact { .. } => None,
|
||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname),
|
||||
},
|
||||
Self::Error(err) => Some(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_text2(&self) -> Option<Cow<str>> {
|
||||
match self {
|
||||
Self::Summary(summary) => Some(summary.truncated_text(160)),
|
||||
Self::Qr(_) => None,
|
||||
Self::Error(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_text1_meaning(&self) -> Meaning {
|
||||
match self {
|
||||
Self::Summary(summary) => match &summary.prefix {
|
||||
None => Meaning::None,
|
||||
Some(SummaryPrefix::Draft(_text)) => Meaning::Text1Draft,
|
||||
Some(SummaryPrefix::Username(_username)) => Meaning::Text1Username,
|
||||
Some(SummaryPrefix::Me(_text)) => Meaning::Text1Self,
|
||||
},
|
||||
Self::Qr(_qr) => Meaning::None,
|
||||
Self::Error(_err) => Meaning::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_state(&self) -> LotState {
|
||||
match self {
|
||||
Self::Summary(summary) => summary.state.into(),
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
|
||||
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
||||
Qr::FprOk { .. } => LotState::QrFprOk,
|
||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
Qr::Url { .. } => LotState::QrUrl,
|
||||
Qr::Text { .. } => LotState::QrText,
|
||||
Qr::WithdrawVerifyContact { .. } => LotState::QrWithdrawVerifyContact,
|
||||
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
||||
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
||||
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
||||
},
|
||||
Self::Error(_err) => LotState::QrError,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> u32 {
|
||||
match self {
|
||||
Self::Summary(_) => Default::default(),
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::AskVerifyGroup { .. } => Default::default(),
|
||||
Qr::FprOk { contact_id } => *contact_id,
|
||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default(),
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Addr { contact_id } => *contact_id,
|
||||
Qr::Url { .. } => Default::default(),
|
||||
Qr::Text { .. } => Default::default(),
|
||||
Qr::WithdrawVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::WithdrawVerifyGroup { .. } => Default::default(),
|
||||
Qr::ReviveVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::ReviveVerifyGroup { .. } => Default::default(),
|
||||
},
|
||||
Self::Error(_) => Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_timestamp(&self) -> i64 {
|
||||
match self {
|
||||
Self::Summary(summary) => summary.timestamp,
|
||||
Self::Qr(_) => Default::default(),
|
||||
Self::Error(_) => Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LotState {
|
||||
// Default
|
||||
Undefined = 0,
|
||||
|
||||
// Qr States
|
||||
/// id=contact
|
||||
QrAskVerifyContact = 200,
|
||||
|
||||
/// text1=groupname
|
||||
QrAskVerifyGroup = 202,
|
||||
|
||||
/// id=contact
|
||||
QrFprOk = 210,
|
||||
|
||||
/// id=contact
|
||||
QrFprMismatch = 220,
|
||||
|
||||
/// text1=formatted fingerprint
|
||||
QrFprWithoutAddr = 230,
|
||||
|
||||
/// text1=domain
|
||||
QrAccount = 250,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
/// id=contact
|
||||
QrAddr = 320,
|
||||
|
||||
/// text1=text
|
||||
QrText = 330,
|
||||
|
||||
/// text1=URL
|
||||
QrUrl = 332,
|
||||
|
||||
/// text1=error string
|
||||
QrError = 400,
|
||||
|
||||
QrWithdrawVerifyContact = 500,
|
||||
|
||||
/// text1=groupname
|
||||
QrWithdrawVerifyGroup = 502,
|
||||
|
||||
QrReviveVerifyContact = 510,
|
||||
|
||||
/// text1=groupname
|
||||
QrReviveVerifyGroup = 512,
|
||||
|
||||
// Message States
|
||||
MsgInFresh = 10,
|
||||
MsgInNoticed = 13,
|
||||
MsgInSeen = 16,
|
||||
MsgOutPreparing = 18,
|
||||
MsgOutDraft = 19,
|
||||
MsgOutPending = 20,
|
||||
MsgOutFailed = 24,
|
||||
MsgOutDelivered = 26,
|
||||
MsgOutMdnRcvd = 28,
|
||||
}
|
||||
|
||||
impl Default for LotState {
|
||||
fn default() -> Self {
|
||||
LotState::Undefined
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MessageState> for LotState {
|
||||
fn from(s: MessageState) -> Self {
|
||||
use MessageState::*;
|
||||
match s {
|
||||
Undefined => LotState::Undefined,
|
||||
InFresh => LotState::MsgInFresh,
|
||||
InNoticed => LotState::MsgInNoticed,
|
||||
InSeen => LotState::MsgInSeen,
|
||||
OutPreparing => LotState::MsgOutPreparing,
|
||||
OutDraft => LotState::MsgOutDraft,
|
||||
OutPending => LotState::MsgOutPending,
|
||||
OutFailed => LotState::MsgOutFailed,
|
||||
OutDelivered => LotState::MsgOutDelivered,
|
||||
OutMdnRcvd => LotState::MsgOutMdnRcvd,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Summary> for Lot {
|
||||
fn from(summary: Summary) -> Self {
|
||||
Lot::Summary(summary)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Qr> for Lot {
|
||||
fn from(qr: Qr) -> Self {
|
||||
Lot::Qr(qr)
|
||||
}
|
||||
}
|
||||
|
||||
// Make it easy to convert errors into the final `Lot`.
|
||||
impl From<Error> for Lot {
|
||||
fn from(error: Error) -> Self {
|
||||
Lot::Error(error.to_string())
|
||||
}
|
||||
}
|
||||
@@ -386,6 +386,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
sendsticker <file> [<text>]\n\
|
||||
sendfile <file> [<text>]\n\
|
||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||
sendsyncmsg\n\
|
||||
videochat\n\
|
||||
draft [<text>]\n\
|
||||
devicemsg <text>\n\
|
||||
@@ -424,6 +425,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
getqr [<chat-id>]\n\
|
||||
getbadqr\n\
|
||||
checkqr <qr-content>\n\
|
||||
joinqr <qr-content>\n\
|
||||
setqr <qr-content>\n\
|
||||
providerinfo <addr>\n\
|
||||
event <event-id to test>\n\
|
||||
@@ -895,6 +897,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}));
|
||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||
}
|
||||
"sendsyncmsg" => match context.send_sync_msg().await? {
|
||||
Some(msg_id) => println!("sync message sent as {}.", msg_id),
|
||||
None => println!("sync message not needed."),
|
||||
},
|
||||
"videochat" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
@@ -1158,14 +1164,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"checkqr" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
let res = check_qr(&context, arg1).await;
|
||||
println!(
|
||||
"state={}, id={}, text1={:?}, text2={:?}",
|
||||
res.get_state(),
|
||||
res.get_id(),
|
||||
res.get_text1(),
|
||||
res.get_text2()
|
||||
);
|
||||
let qr = check_qr(&context, arg1).await?;
|
||||
println!("qr={:?}", qr);
|
||||
}
|
||||
"setqr" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
|
||||
@@ -167,7 +167,7 @@ const DB_COMMANDS: [&str; 10] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 34] = [
|
||||
const CHAT_COMMANDS: [&str; 35] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"chat",
|
||||
@@ -188,6 +188,7 @@ const CHAT_COMMANDS: [&str; 34] = [
|
||||
"sendimage",
|
||||
"sendfile",
|
||||
"sendhtml",
|
||||
"sendsyncmsg",
|
||||
"videochat",
|
||||
"draft",
|
||||
"listmedia",
|
||||
@@ -223,10 +224,11 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"unblock",
|
||||
"listblocked",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 10] = [
|
||||
const MISC_COMMANDS: [&str; 11] = [
|
||||
"getqr",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"joinqr",
|
||||
"event",
|
||||
"fileinfo",
|
||||
"clear",
|
||||
|
||||
@@ -7,3 +7,5 @@
|
||||
cc c310754465ee0261807b96fa9bcc4861ff9aa286e94667524b5960c69f9b6620 # shrinks to buf = "", approx_chars = 0, do_unwrap = false
|
||||
cc 5fd8d730b0a9cdf7308ce58818ca9aefc0255c9ba2a0878944fc48d43a67315b # shrinks to buf = "𑒀ὐ¢🜀\u{1e01b}A a🟠", approx_chars = 0, do_unwrap = false
|
||||
cc c6a0029a54137a4b9efc9ef2ea6d9a7dd1d60d1c937bb472b66a174618ba8013 # shrinks to buf = "𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ ", approx_chars = 0, do_unwrap = false
|
||||
cc 9796807baeda701227dcdcfc9fdaa93ddd556da2bb1630381bfe2e037bee73f6 # shrinks to buf = " ꫛ®a\u{11300}a", approx_chars = 0
|
||||
cc 063a4c42ac1ec9aa37af54521b210ba9cd82dcc9cc3be296ca2fedf8240072d4 # shrinks to buf = "a᪠ 0A", approx_chars = 0
|
||||
|
||||
19
python/mypy.ini
Normal file
19
python/mypy.ini
Normal file
@@ -0,0 +1,19 @@
|
||||
[mypy]
|
||||
|
||||
[mypy-deltachat.capi.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-pluggy.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-cffi.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-imapclient.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-pytest.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-_pytest.*]
|
||||
ignore_missing_imports = True
|
||||
8
python/pyproject.toml
Normal file
8
python/pyproject.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
root = ".."
|
||||
tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
|
||||
git_describe_command = "git describe --dirty --tags --long --match py-*.*"
|
||||
@@ -8,17 +8,11 @@ def main():
|
||||
long_description = f.read()
|
||||
setuptools.setup(
|
||||
name='deltachat',
|
||||
setup_requires=['setuptools_scm', 'cffi>=1.0.0'],
|
||||
use_scm_version = {
|
||||
"root": "..",
|
||||
"relative_to": __file__,
|
||||
'tag_regex': r'^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
|
||||
'git_describe_command': "git describe --dirty --tags --long --match py-*.*",
|
||||
},
|
||||
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
||||
long_description=long_description,
|
||||
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
||||
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient', 'requests'],
|
||||
setup_requires=['setuptools_scm'], # required for compatibility with `python3 setup.py sdist`
|
||||
packages=setuptools.find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||
|
||||
@@ -19,9 +19,9 @@ except DistributionNotFound:
|
||||
|
||||
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||
if not _DC_EVENTNAME_MAP:
|
||||
for name, val in vars(const).items():
|
||||
for name in dir(const):
|
||||
if name.startswith("DC_EVENT_"):
|
||||
_DC_EVENTNAME_MAP[val] = name
|
||||
_DC_EVENTNAME_MAP[getattr(const, name)] = name
|
||||
return _DC_EVENTNAME_MAP[integer]
|
||||
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@ import os
|
||||
from array import array
|
||||
from . import const
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array, DCLot
|
||||
from .chat import Chat
|
||||
from .message import Message
|
||||
from .contact import Contact
|
||||
from .tracker import ImexTracker, ConfigureTracker
|
||||
from . import hookspec
|
||||
from .events import EventThread
|
||||
from typing import Union, Any, Dict, Optional, List, Generator
|
||||
|
||||
|
||||
class MissingCredentials(ValueError):
|
||||
@@ -28,7 +29,7 @@ class Account(object):
|
||||
"""
|
||||
MissingCredentials = MissingCredentials
|
||||
|
||||
def __init__(self, db_path, os_name=None, logging=True):
|
||||
def __init__(self, db_path, os_name=None, logging=True) -> None:
|
||||
""" initialize account object.
|
||||
|
||||
:param db_path: a path to the account database. The database
|
||||
@@ -58,11 +59,11 @@ class Account(object):
|
||||
hook = hookspec.Global._get_plugin_manager().hook
|
||||
hook.dc_account_init(account=self)
|
||||
|
||||
def disable_logging(self):
|
||||
def disable_logging(self) -> None:
|
||||
""" disable logging. """
|
||||
self._logging = False
|
||||
|
||||
def enable_logging(self):
|
||||
def enable_logging(self) -> None:
|
||||
""" re-enable logging. """
|
||||
self._logging = True
|
||||
|
||||
@@ -73,12 +74,12 @@ class Account(object):
|
||||
if self._logging:
|
||||
self._pm.hook.ac_log_line(message=msg)
|
||||
|
||||
def _check_config_key(self, name):
|
||||
def _check_config_key(self, name: str) -> None:
|
||||
if name not in self._configkeys:
|
||||
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
||||
name, self._configkeys))
|
||||
|
||||
def get_info(self):
|
||||
def get_info(self) -> Dict[str, str]:
|
||||
""" return dictionary of built config parameters. """
|
||||
lines = from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
||||
d = {}
|
||||
@@ -105,19 +106,19 @@ class Account(object):
|
||||
cursor += len(entry) + 1
|
||||
log("")
|
||||
|
||||
def set_stock_translation(self, id, string):
|
||||
def set_stock_translation(self, id: int, string: str) -> None:
|
||||
""" set stock translation string.
|
||||
|
||||
:param id: id of stock string (const.DC_STR_*)
|
||||
:param value: string to set as new transalation
|
||||
:returns: None
|
||||
"""
|
||||
string = string.encode("utf8")
|
||||
res = lib.dc_set_stock_translation(self._dc_context, id, string)
|
||||
bytestring = string.encode("utf8")
|
||||
res = lib.dc_set_stock_translation(self._dc_context, id, bytestring)
|
||||
if res == 0:
|
||||
raise ValueError("could not set translation string")
|
||||
|
||||
def set_config(self, name, value):
|
||||
def set_config(self, name: str, value: Optional[str]) -> None:
|
||||
""" set configuration values.
|
||||
|
||||
:param name: config key name (unicode)
|
||||
@@ -125,16 +126,16 @@ class Account(object):
|
||||
:returns: None
|
||||
"""
|
||||
self._check_config_key(name)
|
||||
name = name.encode("utf8")
|
||||
if name == b"addr" and self.is_configured():
|
||||
namebytes = name.encode("utf8")
|
||||
if namebytes == b"addr" and self.is_configured():
|
||||
raise ValueError("can not change 'addr' after account is configured.")
|
||||
if value is not None:
|
||||
value = value.encode("utf8")
|
||||
valuebytes = value.encode("utf8")
|
||||
else:
|
||||
value = ffi.NULL
|
||||
lib.dc_set_config(self._dc_context, name, value)
|
||||
valuebytes = ffi.NULL
|
||||
lib.dc_set_config(self._dc_context, namebytes, valuebytes)
|
||||
|
||||
def get_config(self, name):
|
||||
def get_config(self, name: str) -> str:
|
||||
""" return unicode string value.
|
||||
|
||||
:param name: configuration key to lookup (eg "addr" or "mail_pw")
|
||||
@@ -143,12 +144,12 @@ class Account(object):
|
||||
"""
|
||||
if name != "sys.config_keys":
|
||||
self._check_config_key(name)
|
||||
name = name.encode("utf8")
|
||||
res = lib.dc_get_config(self._dc_context, name)
|
||||
namebytes = name.encode("utf8")
|
||||
res = lib.dc_get_config(self._dc_context, namebytes)
|
||||
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def _preconfigure_keypair(self, addr, public, secret):
|
||||
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
|
||||
"""See dc_preconfigure_keypair() in deltachat.h.
|
||||
|
||||
In other words, you don't need this.
|
||||
@@ -160,7 +161,7 @@ class Account(object):
|
||||
if res == 0:
|
||||
raise Exception("Failed to set key")
|
||||
|
||||
def update_config(self, kwargs):
|
||||
def update_config(self, kwargs: Dict[str, Any]) -> None:
|
||||
""" update config values.
|
||||
|
||||
:param kwargs: name=value config settings for this account.
|
||||
@@ -170,7 +171,7 @@ class Account(object):
|
||||
for key, value in kwargs.items():
|
||||
self.set_config(key, str(value))
|
||||
|
||||
def is_configured(self):
|
||||
def is_configured(self) -> bool:
|
||||
""" determine if the account is configured already; an initial connection
|
||||
to SMTP/IMAP has been verified.
|
||||
|
||||
@@ -178,7 +179,7 @@ class Account(object):
|
||||
"""
|
||||
return True if lib.dc_is_configured(self._dc_context) else False
|
||||
|
||||
def set_avatar(self, img_path):
|
||||
def set_avatar(self, img_path: Optional[str]) -> None:
|
||||
"""Set self avatar.
|
||||
|
||||
:raises ValueError: if profile image could not be set
|
||||
@@ -190,20 +191,18 @@ class Account(object):
|
||||
assert os.path.exists(img_path), img_path
|
||||
self.set_config("selfavatar", img_path)
|
||||
|
||||
def check_is_configured(self):
|
||||
def check_is_configured(self) -> None:
|
||||
""" Raise ValueError if this account is not configured. """
|
||||
if not self.is_configured():
|
||||
raise ValueError("need to configure first")
|
||||
|
||||
def get_latest_backupfile(self, backupdir):
|
||||
def get_latest_backupfile(self, backupdir) -> Optional[str]:
|
||||
""" return the latest backup file in a given directory.
|
||||
"""
|
||||
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
|
||||
if res == ffi.NULL:
|
||||
return None
|
||||
return from_dc_charpointer(res)
|
||||
return from_optional_dc_charpointer(res)
|
||||
|
||||
def get_blobdir(self):
|
||||
def get_blobdir(self) -> str:
|
||||
""" return the directory for files.
|
||||
|
||||
All sent files are copied to this directory if necessary.
|
||||
@@ -211,15 +210,15 @@ class Account(object):
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
|
||||
|
||||
def get_self_contact(self):
|
||||
def get_self_contact(self) -> Contact:
|
||||
""" return this account's identity as a :class:`deltachat.contact.Contact`.
|
||||
|
||||
:returns: :class:`deltachat.contact.Contact`
|
||||
"""
|
||||
return Contact(self, const.DC_CONTACT_ID_SELF)
|
||||
|
||||
def create_contact(self, obj, name=None):
|
||||
""" create a (new) Contact or return an existing one.
|
||||
def create_contact(self, obj, name: Optional[str] = None) -> Contact:
|
||||
"""create a (new) Contact or return an existing one.
|
||||
|
||||
Calling this method will always result in the same
|
||||
underlying contact id. If there already is a Contact
|
||||
@@ -236,13 +235,13 @@ class Account(object):
|
||||
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_contact(self, obj):
|
||||
def get_contact(self, obj) -> Optional[Contact]:
|
||||
if isinstance(obj, Contact):
|
||||
return obj
|
||||
(_, addr) = self.get_contact_addr_and_name(obj)
|
||||
return self.get_contact_by_addr(addr)
|
||||
|
||||
def get_contact_addr_and_name(self, obj, name=None):
|
||||
def get_contact_addr_and_name(self, obj, name: Optional[str] = None):
|
||||
if isinstance(obj, Account):
|
||||
if not obj.is_configured():
|
||||
raise ValueError("can only add addresses from configured accounts")
|
||||
@@ -260,7 +259,7 @@ class Account(object):
|
||||
name = displayname
|
||||
return (name, addr)
|
||||
|
||||
def delete_contact(self, contact):
|
||||
def delete_contact(self, contact: Contact) -> bool:
|
||||
""" delete a Contact.
|
||||
|
||||
:param contact: contact object obtained
|
||||
@@ -271,22 +270,23 @@ class Account(object):
|
||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
||||
|
||||
def get_contact_by_addr(self, email):
|
||||
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
|
||||
""" get a contact for the email address or None if it's blocked or doesn't exist. """
|
||||
_, addr = parseaddr(email)
|
||||
addr = as_dc_charpointer(addr)
|
||||
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
||||
if contact_id:
|
||||
return self.get_contact_by_id(contact_id)
|
||||
return None
|
||||
|
||||
def get_contact_by_id(self, contact_id):
|
||||
""" return Contact instance or None.
|
||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||
""" return Contact instance or raise an exception.
|
||||
:param contact_id: integer id of this contact.
|
||||
:returns: None or :class:`deltachat.contact.Contact` instance.
|
||||
:returns: :class:`deltachat.contact.Contact` instance.
|
||||
"""
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_blocked_contacts(self):
|
||||
def get_blocked_contacts(self) -> List[Contact]:
|
||||
""" return a list of all blocked contacts.
|
||||
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects.
|
||||
@@ -297,8 +297,13 @@ class Account(object):
|
||||
)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||
|
||||
def get_contacts(self, query=None, with_self=False, only_verified=False):
|
||||
""" get a (filtered) list of contacts.
|
||||
def get_contacts(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
with_self: bool = False,
|
||||
only_verified: bool = False,
|
||||
) -> List[Contact]:
|
||||
"""get a (filtered) list of contacts.
|
||||
|
||||
:param query: if a string is specified, only return contacts
|
||||
whose name or e-mail matches query.
|
||||
@@ -318,7 +323,7 @@ class Account(object):
|
||||
)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||
|
||||
def get_fresh_messages(self):
|
||||
def get_fresh_messages(self) -> Generator[Message, None, None]:
|
||||
""" yield all fresh messages from all chats. """
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_fresh_msgs(self._dc_context),
|
||||
@@ -326,12 +331,17 @@ class Account(object):
|
||||
)
|
||||
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
|
||||
|
||||
def create_chat(self, obj):
|
||||
def create_chat(self, obj) -> Chat:
|
||||
""" Create a 1:1 chat with Account, Contact or e-mail address. """
|
||||
return self.create_contact(obj).create_chat()
|
||||
|
||||
def create_group_chat(self, name, contacts=None, verified=False):
|
||||
""" create a new group chat object.
|
||||
def create_group_chat(
|
||||
self,
|
||||
name: str,
|
||||
contacts: Optional[List[Contact]] = None,
|
||||
verified: bool = False,
|
||||
) -> Chat:
|
||||
"""create a new group chat object.
|
||||
|
||||
Chats are unpromoted until the first message is sent.
|
||||
|
||||
@@ -347,7 +357,7 @@ class Account(object):
|
||||
chat.add_contact(contact)
|
||||
return chat
|
||||
|
||||
def get_chats(self):
|
||||
def get_chats(self) -> List[Chat]:
|
||||
""" return list of chats.
|
||||
|
||||
:returns: a list of :class:`deltachat.chat.Chat` objects.
|
||||
@@ -364,17 +374,17 @@ class Account(object):
|
||||
chatlist.append(Chat(self, chat_id))
|
||||
return chatlist
|
||||
|
||||
def get_device_chat(self):
|
||||
def get_device_chat(self) -> Chat:
|
||||
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
|
||||
|
||||
def get_message_by_id(self, msg_id):
|
||||
def get_message_by_id(self, msg_id: int) -> Message:
|
||||
""" return Message instance.
|
||||
:param msg_id: integer id of this message.
|
||||
:returns: :class:`deltachat.message.Message` instance.
|
||||
"""
|
||||
return Message.from_db(self, msg_id)
|
||||
|
||||
def get_chat_by_id(self, chat_id):
|
||||
def get_chat_by_id(self, chat_id: int) -> Chat:
|
||||
""" return Chat instance.
|
||||
:param chat_id: integer id of this chat.
|
||||
:returns: :class:`deltachat.chat.Chat` instance.
|
||||
@@ -386,19 +396,18 @@ class Account(object):
|
||||
lib.dc_chat_unref(res)
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def mark_seen_messages(self, messages):
|
||||
def mark_seen_messages(self, messages: List[Union[int, Message]]) -> None:
|
||||
""" mark the given set of messages as seen.
|
||||
|
||||
:param messages: a list of message ids or Message instances.
|
||||
"""
|
||||
arr = array("i")
|
||||
for msg in messages:
|
||||
msg = getattr(msg, "id", msg)
|
||||
arr.append(msg)
|
||||
arr.append(getattr(msg, "id", msg))
|
||||
msg_ids = ffi.cast("uint32_t*", ffi.from_buffer(arr))
|
||||
lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages))
|
||||
|
||||
def forward_messages(self, messages, chat):
|
||||
def forward_messages(self, messages: List[Message], chat: Chat) -> None:
|
||||
""" Forward list of messages to a chat.
|
||||
|
||||
:param messages: list of :class:`deltachat.message.Message` object.
|
||||
@@ -408,7 +417,7 @@ class Account(object):
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
|
||||
|
||||
def delete_messages(self, messages):
|
||||
def delete_messages(self, messages: List[Message]) -> None:
|
||||
""" delete messages (local and remote).
|
||||
|
||||
:param messages: list of :class:`deltachat.message.Message` object.
|
||||
@@ -466,7 +475,7 @@ class Account(object):
|
||||
def imex(self, path, imex_cmd):
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
|
||||
def initiate_key_transfer(self):
|
||||
def initiate_key_transfer(self) -> str:
|
||||
"""return setup code after a Autocrypt setup message
|
||||
has been successfully sent to our own e-mail address ("self-sent message").
|
||||
If sending out was unsuccessful, a RuntimeError is raised.
|
||||
@@ -477,7 +486,7 @@ class Account(object):
|
||||
raise RuntimeError("could not send out autocrypt setup message")
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_setup_contact_qr(self):
|
||||
def get_setup_contact_qr(self) -> str:
|
||||
""" get/create Setup-Contact QR Code as ascii-string.
|
||||
|
||||
this string needs to be transferred to another DC account
|
||||
@@ -527,7 +536,9 @@ class Account(object):
|
||||
raise ValueError("could not join group")
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
||||
def set_location(
|
||||
self, latitude: float = 0.0, longitude: float = 0.0, accuracy: float = 0.0
|
||||
) -> None:
|
||||
"""set a new location. It effects all chats where we currently
|
||||
have enabled location streaming.
|
||||
|
||||
@@ -571,7 +582,7 @@ class Account(object):
|
||||
def get_connectivity(self):
|
||||
return lib.dc_get_connectivity(self._dc_context)
|
||||
|
||||
def get_connectivity_html(self):
|
||||
def get_connectivity_html(self) -> str:
|
||||
return from_dc_charpointer(lib.dc_get_connectivity_html(self._dc_context))
|
||||
|
||||
def all_work_done(self):
|
||||
@@ -621,7 +632,7 @@ class Account(object):
|
||||
"""
|
||||
lib.dc_maybe_network(self._dc_context)
|
||||
|
||||
def configure(self, reconfigure=False):
|
||||
def configure(self, reconfigure: bool = False) -> ConfigureTracker:
|
||||
""" Start configuration process and return a Configtracker instance
|
||||
on which you can block with wait_finish() to get a True/False success
|
||||
value for the configuration process.
|
||||
@@ -634,11 +645,11 @@ class Account(object):
|
||||
lib.dc_configure(self._dc_context)
|
||||
return configtracker
|
||||
|
||||
def wait_shutdown(self):
|
||||
def wait_shutdown(self) -> None:
|
||||
""" wait until shutdown of this account has completed. """
|
||||
self._shutdown_event.wait()
|
||||
|
||||
def stop_io(self):
|
||||
def stop_io(self) -> None:
|
||||
""" stop core IO scheduler if it is running. """
|
||||
self.log("stop_ongoing")
|
||||
self.stop_ongoing()
|
||||
@@ -646,7 +657,7 @@ class Account(object):
|
||||
self.log("dc_stop_io (stop core IO scheduler)")
|
||||
lib.dc_stop_io(self._dc_context)
|
||||
|
||||
def shutdown(self):
|
||||
def shutdown(self) -> None:
|
||||
""" shutdown and destroy account (stop callback thread, close and remove
|
||||
underlying dc_context)."""
|
||||
if self._dc_context is None:
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import mimetypes
|
||||
import calendar
|
||||
import json
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
||||
from .capi import lib, ffi
|
||||
from . import const
|
||||
from .message import Message
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Chat(object):
|
||||
@@ -17,20 +18,20 @@ class Chat(object):
|
||||
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||
"""
|
||||
|
||||
def __init__(self, account, id):
|
||||
def __init__(self, account, id) -> None:
|
||||
from .account import Account
|
||||
assert isinstance(account, Account), repr(account)
|
||||
self.account = account
|
||||
self.id = id
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other) -> bool:
|
||||
return self.id == getattr(other, "id", None) and \
|
||||
self.account._dc_context == other.account._dc_context
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other) -> bool:
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return "<Chat id={} name={}>".format(self.id, self.get_name())
|
||||
|
||||
@property
|
||||
@@ -40,7 +41,7 @@ class Chat(object):
|
||||
lib.dc_chat_unref
|
||||
)
|
||||
|
||||
def delete(self):
|
||||
def delete(self) -> None:
|
||||
"""Delete this chat and all its messages.
|
||||
|
||||
Note:
|
||||
@@ -50,24 +51,24 @@ class Chat(object):
|
||||
"""
|
||||
lib.dc_delete_chat(self.account._dc_context, self.id)
|
||||
|
||||
def block(self):
|
||||
def block(self) -> None:
|
||||
"""Block this chat."""
|
||||
lib.dc_block_chat(self.account._dc_context, self.id)
|
||||
|
||||
def accept(self):
|
||||
def accept(self) -> None:
|
||||
"""Accept this contact request chat."""
|
||||
lib.dc_accept_chat(self.account._dc_context, self.id)
|
||||
|
||||
# ------ chat status/metadata API ------------------------------
|
||||
|
||||
def is_group(self):
|
||||
def is_group(self) -> bool:
|
||||
""" return true if this chat is a group chat.
|
||||
|
||||
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
|
||||
"""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
|
||||
|
||||
def is_muted(self):
|
||||
def is_muted(self) -> bool:
|
||||
""" return true if this chat is muted.
|
||||
|
||||
:returns: True if chat is muted, False otherwise.
|
||||
@@ -90,7 +91,7 @@ class Chat(object):
|
||||
"""
|
||||
return not lib.dc_chat_is_unpromoted(self._dc_chat)
|
||||
|
||||
def can_send(self):
|
||||
def can_send(self) -> bool:
|
||||
"""Check if messages can be sent to a give chat.
|
||||
This is not true eg. for the contact requests or for the device-talk
|
||||
|
||||
@@ -98,30 +99,30 @@ class Chat(object):
|
||||
"""
|
||||
return lib.dc_chat_can_send(self._dc_chat)
|
||||
|
||||
def is_protected(self):
|
||||
def is_protected(self) -> bool:
|
||||
""" return True if this chat is a protected chat.
|
||||
|
||||
:returns: True if chat is protected, False otherwise.
|
||||
"""
|
||||
return lib.dc_chat_is_protected(self._dc_chat)
|
||||
|
||||
def get_name(self):
|
||||
def get_name(self) -> Optional[str]:
|
||||
""" return name of this chat.
|
||||
|
||||
:returns: unicode name
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_chat_get_name(self._dc_chat))
|
||||
|
||||
def set_name(self, name):
|
||||
def set_name(self, name: str) -> bool:
|
||||
""" set name of this chat.
|
||||
|
||||
:param name: as a unicode string.
|
||||
:returns: None
|
||||
:returns: True on success, False otherwise
|
||||
"""
|
||||
name = as_dc_charpointer(name)
|
||||
return lib.dc_set_chat_name(self.account._dc_context, self.id, name)
|
||||
return bool(lib.dc_set_chat_name(self.account._dc_context, self.id, name))
|
||||
|
||||
def mute(self, duration=None):
|
||||
def mute(self, duration: Optional[int] = None) -> None:
|
||||
""" mutes the chat
|
||||
|
||||
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
|
||||
@@ -135,7 +136,7 @@ class Chat(object):
|
||||
if not bool(ret):
|
||||
raise ValueError("Call to dc_set_chat_mute_duration failed")
|
||||
|
||||
def unmute(self):
|
||||
def unmute(self) -> None:
|
||||
""" unmutes the chat
|
||||
|
||||
:returns: None
|
||||
@@ -144,7 +145,7 @@ class Chat(object):
|
||||
if not bool(ret):
|
||||
raise ValueError("Failed to unmute chat")
|
||||
|
||||
def get_mute_duration(self):
|
||||
def get_mute_duration(self) -> int:
|
||||
""" Returns the number of seconds until the mute of this chat is lifted.
|
||||
|
||||
:param duration:
|
||||
@@ -152,37 +153,37 @@ class Chat(object):
|
||||
"""
|
||||
return lib.dc_chat_get_remaining_mute_duration(self._dc_chat)
|
||||
|
||||
def get_ephemeral_timer(self):
|
||||
def get_ephemeral_timer(self) -> int:
|
||||
""" get ephemeral timer.
|
||||
|
||||
:returns: ephemeral timer value in seconds
|
||||
"""
|
||||
return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id)
|
||||
|
||||
def set_ephemeral_timer(self, timer):
|
||||
def set_ephemeral_timer(self, timer: int) -> bool:
|
||||
""" set ephemeral timer.
|
||||
|
||||
:param: timer value in seconds
|
||||
|
||||
:returns: None
|
||||
:returns: True on success, False otherwise
|
||||
"""
|
||||
return lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer)
|
||||
return bool(lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer))
|
||||
|
||||
def get_type(self):
|
||||
def get_type(self) -> int:
|
||||
""" (deprecated) return type of this chat.
|
||||
|
||||
:returns: one of const.DC_CHAT_TYPE_*
|
||||
"""
|
||||
return lib.dc_chat_get_type(self._dc_chat)
|
||||
|
||||
def get_encryption_info(self):
|
||||
def get_encryption_info(self) -> Optional[str]:
|
||||
"""Return encryption info for this chat.
|
||||
|
||||
:returns: a string with encryption preferences of all chat members"""
|
||||
res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id)
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_join_qr(self):
|
||||
def get_join_qr(self) -> Optional[str]:
|
||||
""" get/create Join-Group QR Code as ascii-string.
|
||||
|
||||
this string needs to be transferred to another DC account
|
||||
@@ -194,7 +195,7 @@ class Chat(object):
|
||||
|
||||
# ------ chat messaging API ------------------------------
|
||||
|
||||
def send_msg(self, msg):
|
||||
def send_msg(self, msg: Message) -> Message:
|
||||
"""send a message by using a ready Message object.
|
||||
|
||||
:param msg: a :class:`deltachat.message.Message` instance
|
||||
@@ -512,8 +513,9 @@ class Chat(object):
|
||||
latitude=lib.dc_array_get_latitude(dc_array, i),
|
||||
longitude=lib.dc_array_get_longitude(dc_array, i),
|
||||
accuracy=lib.dc_array_get_accuracy(dc_array, i),
|
||||
timestamp=datetime.utcfromtimestamp(
|
||||
lib.dc_array_get_timestamp(dc_array, i)
|
||||
timestamp=datetime.fromtimestamp(
|
||||
lib.dc_array_get_timestamp(dc_array, i),
|
||||
timezone.utc
|
||||
),
|
||||
marker=from_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
from typing import Any, List
|
||||
|
||||
from .capi import lib
|
||||
|
||||
|
||||
for name in dir(lib):
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name.startswith("DC_"):
|
||||
globals()[name] = getattr(lib, name)
|
||||
del name
|
||||
return getattr(lib, name)
|
||||
return globals()[name]
|
||||
|
||||
|
||||
def __dir__() -> List[str]:
|
||||
return sorted(name for name in dir(lib) if name.startswith("DC_"))
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
""" Contact object. """
|
||||
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from . import const, props
|
||||
from .capi import ffi, lib
|
||||
from .chat import Chat
|
||||
from . import const
|
||||
from .cutil import from_dc_charpointer, from_optional_dc_charpointer
|
||||
|
||||
|
||||
class Contact(object):
|
||||
@@ -35,18 +37,25 @@ class Contact(object):
|
||||
)
|
||||
|
||||
@props.with_doc
|
||||
def addr(self):
|
||||
def addr(self) -> str:
|
||||
""" normalized e-mail address for this account. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
||||
|
||||
@props.with_doc
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
""" display name for this contact. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
||||
|
||||
# deprecated alias
|
||||
display_name = name
|
||||
|
||||
@props.with_doc
|
||||
def last_seen(self) -> date:
|
||||
"""Last seen timestamp."""
|
||||
return datetime.fromtimestamp(
|
||||
lib.dc_contact_get_last_seen(self._dc_contact), timezone.utc
|
||||
)
|
||||
|
||||
def is_blocked(self):
|
||||
""" Return True if the contact is blocked. """
|
||||
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||
@@ -67,15 +76,13 @@ class Contact(object):
|
||||
""" Return True if the contact is verified. """
|
||||
return lib.dc_contact_is_verified(self._dc_contact)
|
||||
|
||||
def get_profile_image(self):
|
||||
def get_profile_image(self) -> Optional[str]:
|
||||
"""Get contact profile image.
|
||||
|
||||
:returns: path to profile image, None if no profile image exists.
|
||||
"""
|
||||
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
|
||||
if dc_res == ffi.NULL:
|
||||
return None
|
||||
return from_dc_charpointer(dc_res)
|
||||
return from_optional_dc_charpointer(dc_res)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from .capi import lib
|
||||
from .capi import ffi
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, TypeVar, Generator, Callable
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def as_dc_charpointer(obj):
|
||||
@@ -11,21 +14,28 @@ def as_dc_charpointer(obj):
|
||||
return obj
|
||||
|
||||
|
||||
def iter_array(dc_array_t, constructor):
|
||||
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
|
||||
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
|
||||
yield constructor(lib.dc_array_get_id(dc_array_t, i))
|
||||
|
||||
|
||||
def from_dc_charpointer(obj):
|
||||
def from_dc_charpointer(obj) -> str:
|
||||
if obj != ffi.NULL:
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
raise ValueError
|
||||
|
||||
|
||||
def from_optional_dc_charpointer(obj) -> Optional[str]:
|
||||
if obj != ffi.NULL:
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
return None
|
||||
|
||||
|
||||
class DCLot:
|
||||
def __init__(self, dc_lot):
|
||||
def __init__(self, dc_lot) -> None:
|
||||
self._dc_lot = dc_lot
|
||||
|
||||
def id(self):
|
||||
def id(self) -> int:
|
||||
return lib.dc_lot_get_id(self._dc_lot)
|
||||
|
||||
def state(self):
|
||||
@@ -44,4 +54,4 @@ class DCLot:
|
||||
ts = lib.dc_lot_get_timestamp(self._dc_lot)
|
||||
if ts == 0:
|
||||
return None
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
@@ -11,7 +11,7 @@ from imapclient import IMAPClient
|
||||
from imapclient.exceptions import IMAPClientError
|
||||
import imaplib
|
||||
import deltachat
|
||||
from deltachat import const
|
||||
from deltachat import const, Account
|
||||
|
||||
|
||||
SEEN = b'\\Seen'
|
||||
@@ -62,7 +62,7 @@ def dc_account_after_shutdown(account):
|
||||
|
||||
|
||||
class DirectImap:
|
||||
def __init__(self, account):
|
||||
def __init__(self, account: Account) -> None:
|
||||
self.account = account
|
||||
self.logid = account.get_config("displayname") or id(account)
|
||||
self._idling = False
|
||||
|
||||
@@ -9,11 +9,11 @@ from .hookspec import account_hookimpl
|
||||
from contextlib import contextmanager
|
||||
from .capi import ffi, lib
|
||||
from .message import map_system_message
|
||||
from .cutil import from_dc_charpointer
|
||||
from .cutil import from_optional_dc_charpointer
|
||||
|
||||
|
||||
class FFIEvent:
|
||||
def __init__(self, name, data1, data2):
|
||||
def __init__(self, name: str, data1, data2):
|
||||
self.name = name
|
||||
self.data1 = data1
|
||||
self.data2 = data2
|
||||
@@ -29,13 +29,13 @@ class FFIEventLogger:
|
||||
# to prevent garbled logging
|
||||
_loglock = threading.RLock()
|
||||
|
||||
def __init__(self, account):
|
||||
def __init__(self, account) -> None:
|
||||
self.account = account
|
||||
self.logid = self.account.get_config("displayname")
|
||||
self.init_time = time.time()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
def ac_process_ffi_event(self, ffi_event: FFIEvent) -> None:
|
||||
self.account.log(str(ffi_event))
|
||||
|
||||
@account_hookimpl
|
||||
@@ -69,7 +69,7 @@ class FFIEventTracker:
|
||||
self._event_queue = Queue()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
def ac_process_ffi_event(self, ffi_event: FFIEvent):
|
||||
self._event_queue.put(ffi_event)
|
||||
|
||||
def set_timeout(self, timeout):
|
||||
@@ -96,7 +96,7 @@ class FFIEventTracker:
|
||||
if rex.match(ev.name):
|
||||
return ev
|
||||
|
||||
def get_info_contains(self, regex):
|
||||
def get_info_contains(self, regex: str) -> FFIEvent:
|
||||
rex = re.compile(regex)
|
||||
while 1:
|
||||
ev = self.get_matching("DC_EVENT_INFO")
|
||||
@@ -176,6 +176,7 @@ class FFIEventTracker:
|
||||
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
if ev.data2 > 0:
|
||||
return self.account.get_message_by_id(ev.data2)
|
||||
return None
|
||||
|
||||
def wait_msg_delivered(self, msg):
|
||||
ev = self.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
@@ -189,7 +190,7 @@ class EventThread(threading.Thread):
|
||||
|
||||
With each Account init this callback thread is started.
|
||||
"""
|
||||
def __init__(self, account):
|
||||
def __init__(self, account) -> None:
|
||||
self.account = account
|
||||
super(EventThread, self).__init__(name="events")
|
||||
self.setDaemon(True)
|
||||
@@ -202,17 +203,17 @@ class EventThread(threading.Thread):
|
||||
yield
|
||||
self.account.log(message + " FINISHED")
|
||||
|
||||
def mark_shutdown(self):
|
||||
def mark_shutdown(self) -> None:
|
||||
self._marked_for_shutdown = True
|
||||
|
||||
def wait(self, timeout=None):
|
||||
def wait(self, timeout=None) -> None:
|
||||
if self == threading.current_thread():
|
||||
# we are in the callback thread and thus cannot
|
||||
# wait for the thread-loop to finish.
|
||||
return
|
||||
self.join(timeout=timeout)
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
""" get and run events until shutdown. """
|
||||
with self.log_execution("EVENT THREAD"):
|
||||
self._inner_run()
|
||||
@@ -234,7 +235,7 @@ class EventThread(threading.Thread):
|
||||
# function which provides us signature info of an event call
|
||||
evt_name = deltachat.get_dc_event_name(evt)
|
||||
if lib.dc_event_has_string_data(evt):
|
||||
data2 = from_dc_charpointer(lib.dc_event_get_data2_str(event))
|
||||
data2 = from_optional_dc_charpointer(lib.dc_event_get_data2_str(event))
|
||||
else:
|
||||
data2 = lib.dc_event_get_data2_int(event)
|
||||
|
||||
@@ -250,7 +251,7 @@ class EventThread(threading.Thread):
|
||||
if self.account._dc_context is not None:
|
||||
raise
|
||||
|
||||
def _map_ffi_event(self, ffi_event):
|
||||
def _map_ffi_event(self, ffi_event: FFIEvent):
|
||||
name = ffi_event.name
|
||||
account = self.account
|
||||
if name == "DC_EVENT_CONFIGURE_PROGRESS":
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import os
|
||||
import re
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer, as_dc_charpointer
|
||||
from .cutil import from_dc_charpointer, from_optional_dc_charpointer, as_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
from . import const
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Message(object):
|
||||
@@ -75,7 +76,7 @@ class Message(object):
|
||||
return lib.dc_msg_get_id(self._dc_msg)
|
||||
|
||||
@props.with_doc
|
||||
def text(self):
|
||||
def text(self) -> str:
|
||||
"""unicode text of this messages (might be empty if not a text message). """
|
||||
return from_dc_charpointer(lib.dc_msg_get_text(self._dc_msg))
|
||||
|
||||
@@ -84,9 +85,9 @@ class Message(object):
|
||||
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
|
||||
|
||||
@props.with_doc
|
||||
def html(self):
|
||||
def html(self) -> str:
|
||||
"""html text of this messages (might be empty if not an html message). """
|
||||
return from_dc_charpointer(
|
||||
return from_optional_dc_charpointer(
|
||||
lib.dc_get_msg_html(self.account._dc_context, self.id)) or ""
|
||||
|
||||
def has_html(self):
|
||||
@@ -113,12 +114,13 @@ class Message(object):
|
||||
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
|
||||
|
||||
@props.with_doc
|
||||
def basename(self):
|
||||
def basename(self) -> str:
|
||||
"""basename of the attachment if it exists, otherwise empty string. """
|
||||
# FIXME, it does not return basename
|
||||
return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg))
|
||||
|
||||
@props.with_doc
|
||||
def filemime(self):
|
||||
def filemime(self) -> str:
|
||||
"""mime type of the file (if it exists)"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
||||
|
||||
@@ -130,7 +132,7 @@ class Message(object):
|
||||
""" return True if this message is a setup message. """
|
||||
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
||||
|
||||
def get_setupcodebegin(self):
|
||||
def get_setupcodebegin(self) -> str:
|
||||
""" return the first characters of a setup code in a setup message. """
|
||||
return from_dc_charpointer(lib.dc_msg_get_setupcodebegin(self._dc_msg))
|
||||
|
||||
@@ -146,7 +148,7 @@ class Message(object):
|
||||
""" return True if this message was forwarded. """
|
||||
return bool(lib.dc_msg_is_forwarded(self._dc_msg))
|
||||
|
||||
def get_message_info(self):
|
||||
def get_message_info(self) -> str:
|
||||
""" Return informational text for a single message.
|
||||
|
||||
The text is multiline and may contain eg. the raw text of the message.
|
||||
@@ -170,7 +172,7 @@ class Message(object):
|
||||
:returns: naive datetime.datetime() object.
|
||||
"""
|
||||
ts = lib.dc_msg_get_timestamp(self._dc_msg)
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
@props.with_doc
|
||||
def time_received(self):
|
||||
@@ -180,7 +182,7 @@ class Message(object):
|
||||
"""
|
||||
ts = lib.dc_msg_get_received_timestamp(self._dc_msg)
|
||||
if ts:
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
@props.with_doc
|
||||
def ephemeral_timer(self):
|
||||
@@ -200,14 +202,14 @@ class Message(object):
|
||||
"""
|
||||
ts = lib.dc_msg_get_ephemeral_timestamp(self._dc_msg)
|
||||
if ts:
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
@property
|
||||
def quoted_text(self):
|
||||
def quoted_text(self) -> Optional[str]:
|
||||
"""Text inside the quote
|
||||
|
||||
:returns: Quoted text"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
|
||||
|
||||
@property
|
||||
def quote(self):
|
||||
@@ -240,9 +242,9 @@ class Message(object):
|
||||
return email.message_from_string(s)
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
def error(self) -> Optional[str]:
|
||||
"""Error message"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
|
||||
|
||||
@property
|
||||
def chat(self):
|
||||
@@ -255,12 +257,12 @@ class Message(object):
|
||||
return Chat(self.account, chat_id)
|
||||
|
||||
@props.with_doc
|
||||
def override_sender_name(self):
|
||||
def override_sender_name(self) -> Optional[str]:
|
||||
"""the name that should be shown over the message instead of the contact display name.
|
||||
|
||||
Usually used to impersonate someone else.
|
||||
"""
|
||||
return from_dc_charpointer(
|
||||
return from_optional_dc_charpointer(
|
||||
lib.dc_msg_get_override_sender_name(self._dc_msg))
|
||||
|
||||
def set_override_sender_name(self, name):
|
||||
|
||||
@@ -14,7 +14,7 @@ class Provider(object):
|
||||
:param domain: The email to get the provider info for.
|
||||
"""
|
||||
|
||||
def __init__(self, account, addr):
|
||||
def __init__(self, account, addr) -> None:
|
||||
provider = ffi.gc(
|
||||
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
|
||||
lib.dc_provider_unref,
|
||||
@@ -24,19 +24,19 @@ class Provider(object):
|
||||
self._provider = provider
|
||||
|
||||
@property
|
||||
def overview_page(self):
|
||||
def overview_page(self) -> str:
|
||||
"""URL to the overview page of the provider on providers.delta.chat."""
|
||||
return from_dc_charpointer(
|
||||
lib.dc_provider_get_overview_page(self._provider))
|
||||
|
||||
@property
|
||||
def get_before_login_hints(self):
|
||||
def get_before_login_hints(self) -> str:
|
||||
"""Should be shown to the user on login."""
|
||||
return from_dc_charpointer(
|
||||
lib.dc_provider_get_before_login_hints(self._provider))
|
||||
lib.dc_provider_get_before_login_hint(self._provider))
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
def status(self) -> int:
|
||||
"""The status of the provider information.
|
||||
|
||||
This is one of the
|
||||
|
||||
@@ -9,6 +9,7 @@ import fnmatch
|
||||
import time
|
||||
import weakref
|
||||
import tempfile
|
||||
from typing import List, Dict, Callable
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
@@ -126,7 +127,7 @@ def pytest_report_header(config, startdir):
|
||||
|
||||
|
||||
class SessionLiveConfigFromFile:
|
||||
def __init__(self, fn):
|
||||
def __init__(self, fn) -> None:
|
||||
self.fn = fn
|
||||
self.configlist = []
|
||||
for line in open(fn):
|
||||
@@ -137,19 +138,21 @@ class SessionLiveConfigFromFile:
|
||||
d[name] = value
|
||||
self.configlist.append(d)
|
||||
|
||||
def get(self, index):
|
||||
def get(self, index: int):
|
||||
return self.configlist[index]
|
||||
|
||||
def exists(self):
|
||||
def exists(self) -> bool:
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
class SessionLiveConfigFromURL:
|
||||
def __init__(self, url):
|
||||
configlist: List[Dict[str, str]]
|
||||
|
||||
def __init__(self, url: str) -> None:
|
||||
self.configlist = []
|
||||
self.url = url
|
||||
|
||||
def get(self, index):
|
||||
def get(self, index: int):
|
||||
try:
|
||||
return self.configlist[index]
|
||||
except IndexError:
|
||||
@@ -162,7 +165,7 @@ class SessionLiveConfigFromURL:
|
||||
self.configlist.append(config)
|
||||
return config
|
||||
|
||||
def exists(self):
|
||||
def exists(self) -> bool:
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
@@ -179,7 +182,7 @@ def session_liveconfig(request):
|
||||
@pytest.fixture
|
||||
def data(request):
|
||||
class Data:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
# trying to find test data heuristically
|
||||
# because we are run from a dev-setup with pytest direct,
|
||||
# through tox, and then maybe also from deltachat-binding
|
||||
@@ -210,7 +213,10 @@ def data(request):
|
||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
|
||||
class AccountMaker:
|
||||
def __init__(self):
|
||||
_finalizers: List[Callable[[], None]]
|
||||
_accounts: List[Account]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.live_count = 0
|
||||
self.offline_count = 0
|
||||
self._finalizers = []
|
||||
@@ -423,7 +429,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
pass
|
||||
imap.dump_imap_structures(tmpdir, logfile=logfile)
|
||||
|
||||
def get_accepted_chat(self, ac1, ac2):
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account):
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
@@ -451,7 +457,9 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
|
||||
|
||||
class BotProcess:
|
||||
def __init__(self, popen, bot_cfg):
|
||||
stdout_queue: queue.Queue
|
||||
|
||||
def __init__(self, popen, bot_cfg) -> None:
|
||||
self.popen = popen
|
||||
self.addr = bot_cfg["addr"]
|
||||
|
||||
@@ -459,10 +467,10 @@ class BotProcess:
|
||||
# the (unicode) lines available for readers through a queue.
|
||||
self.stdout_queue = queue.Queue()
|
||||
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread")
|
||||
t.setDaemon(1)
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
|
||||
def _run_stdout_thread(self):
|
||||
def _run_stdout_thread(self) -> None:
|
||||
try:
|
||||
while 1:
|
||||
line = self.popen.stdout.readline()
|
||||
@@ -474,10 +482,10 @@ class BotProcess:
|
||||
finally:
|
||||
self.stdout_queue.put(None)
|
||||
|
||||
def kill(self):
|
||||
def kill(self) -> None:
|
||||
self.popen.kill()
|
||||
|
||||
def wait(self, timeout=30):
|
||||
def wait(self, timeout=30) -> None:
|
||||
self.popen.wait(timeout=timeout)
|
||||
|
||||
def fnmatch_lines(self, pattern_lines):
|
||||
@@ -509,14 +517,14 @@ def tmp_db_path(tmpdir):
|
||||
@pytest.fixture
|
||||
def lp():
|
||||
class Printer:
|
||||
def sec(self, msg):
|
||||
def sec(self, msg: str) -> None:
|
||||
print()
|
||||
print("=" * 10, msg, "=" * 10)
|
||||
|
||||
def step(self, msg):
|
||||
def step(self, msg: str) -> None:
|
||||
print("-" * 5, "step " + msg, "-" * 5)
|
||||
|
||||
def indent(self, msg):
|
||||
def indent(self, msg: str) -> None:
|
||||
print(" " + msg)
|
||||
|
||||
return Printer()
|
||||
|
||||
@@ -10,7 +10,7 @@ from deltachat.tracker import ImexTracker
|
||||
from deltachat.hookspec import account_hookimpl
|
||||
from deltachat.capi import ffi, lib
|
||||
from deltachat.cutil import iter_array
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
|
||||
@pytest.mark.parametrize("msgtext,res", [
|
||||
@@ -265,23 +265,23 @@ class TestOfflineChat:
|
||||
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
|
||||
|
||||
def test_group_chat_creation_with_translation(self, ac1):
|
||||
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %1$s")
|
||||
ac1.set_stock_translation(const.DC_STR_MSGGRPNAME, "abc %1$s xyz %2$s")
|
||||
ac1._evtracker.consume_events()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %2$s")
|
||||
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(500, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some2@example.org", name="some2")
|
||||
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
|
||||
assert chat.get_name() == "title1"
|
||||
assert contact1 in chat.get_contacts()
|
||||
assert contact2 in chat.get_contacts()
|
||||
assert not chat.is_promoted()
|
||||
msg = chat.get_draft()
|
||||
assert msg.text == "xyz title1"
|
||||
chat = ac1.create_group_chat(name="homework", contacts=[])
|
||||
assert chat.get_name() == "homework"
|
||||
chat.send_text("Now we have a group for homework")
|
||||
assert chat.is_promoted()
|
||||
chat.set_name("Homework")
|
||||
assert chat.get_messages()[-1].text == "abc homework xyz Homework by me."
|
||||
|
||||
@pytest.mark.parametrize("verified", [True, False])
|
||||
def test_group_chat_qr(self, acfactory, ac1, verified):
|
||||
@@ -447,7 +447,7 @@ class TestOfflineChat:
|
||||
contact1.create_chat().send_text("hello")
|
||||
|
||||
def test_chat_message_distinctions(self, ac1, chat1):
|
||||
past1s = datetime.utcnow() - timedelta(seconds=1)
|
||||
past1s = datetime.now(timezone.utc) - timedelta(seconds=1)
|
||||
msg = chat1.send_text("msg1")
|
||||
ts = msg.time_sent
|
||||
assert msg.time_received is None
|
||||
@@ -1221,6 +1221,40 @@ class TestOnlineAccount:
|
||||
assert not msg.is_encrypted()
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
|
||||
def test_gossip_optimization(self, acfactory, lp):
|
||||
"""Test that gossip timestamp is updated when someone else sends gossip,
|
||||
so we don't have to send gossip ourselves.
|
||||
"""
|
||||
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
|
||||
|
||||
acfactory.introduce_each_other([ac1, ac2])
|
||||
acfactory.introduce_each_other([ac2, ac3])
|
||||
|
||||
lp.sec("ac1 creates a group chat with ac2")
|
||||
group_chat = ac1.create_group_chat("hello")
|
||||
group_chat.add_contact(ac2)
|
||||
msg = group_chat.send_text("hi")
|
||||
|
||||
# No Autocrypt gossip was sent yet.
|
||||
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
|
||||
assert gossiped_timestamp == 0
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
assert msg.text == "hi"
|
||||
|
||||
lp.sec("ac2 adds ac3 to the group")
|
||||
msg.chat.add_contact(ac3)
|
||||
|
||||
lp.sec("ac1 receives message from ac2 and updates gossip timestamp")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
|
||||
# ac1 has updated the gossip timestamp even though no gossip was sent by ac1.
|
||||
# ac1 does not need to send gossip because ac2 already did it.
|
||||
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
|
||||
assert gossiped_timestamp == int(msg.time_sent.timestamp())
|
||||
|
||||
def test_gossip_encryption_preference(self, acfactory, lp):
|
||||
"""Test that encryption preference of group members is gossiped to new members.
|
||||
This is a Delta Chat extension to Autocrypt 1.1.0, which Autocrypt-Gossip headers
|
||||
@@ -2163,7 +2197,7 @@ class TestOnlineAccount:
|
||||
break # DC is done with reading messages
|
||||
|
||||
def test_send_receive_locations(self, acfactory, lp):
|
||||
now = datetime.utcnow()
|
||||
now = datetime.now(timezone.utc)
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
[tox]
|
||||
isolated_build = true
|
||||
envlist =
|
||||
py3
|
||||
lint
|
||||
mypy
|
||||
auditwheels
|
||||
|
||||
[testenv]
|
||||
@@ -42,12 +44,20 @@ commands =
|
||||
flake8 tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
[testenv:mypy]
|
||||
deps =
|
||||
mypy
|
||||
typing
|
||||
types-setuptools
|
||||
types-requests
|
||||
commands =
|
||||
mypy --no-incremental src/
|
||||
|
||||
[testenv:doc]
|
||||
changedir=doc
|
||||
deps =
|
||||
# Pin dependencies to the versions which actually work with Python 3.5.
|
||||
sphinx==3.4.3
|
||||
breathe==4.28.0
|
||||
sphinx
|
||||
breathe
|
||||
commands =
|
||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.6 environment as the base environment
|
||||
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
|
||||
# we use the python3.7 environment as the base environment
|
||||
/opt/python/cp37-cp37m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.6 environment as the base environment
|
||||
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
|
||||
# we use the python3.7 environment as the base environment
|
||||
/opt/python/cp37-cp37m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
|
||||
@@ -19,11 +19,9 @@ export DCC_RS_TARGET=release
|
||||
|
||||
# Configure access to a base python and to several python interpreters
|
||||
# needed by tox below.
|
||||
export PATH=$PATH:/opt/python/cp36-cp36m/bin
|
||||
export PATH=$PATH:/opt/python/cp37-cp37m/bin
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
pushd /bin
|
||||
rm -f python3.6
|
||||
ln -s /opt/python/cp36-cp36m/bin/python3.6
|
||||
rm -f python3.7
|
||||
ln -s /opt/python/cp37-cp37m/bin/python3.7
|
||||
rm -f python3.8
|
||||
|
||||
@@ -621,7 +621,10 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::{message::Message, test_utils::TestContext};
|
||||
use crate::{
|
||||
message::Message,
|
||||
test_utils::{self, TestContext},
|
||||
};
|
||||
use image::Pixel;
|
||||
|
||||
#[async_std::test]
|
||||
@@ -870,11 +873,10 @@ mod tests {
|
||||
async fn test_selfavatar_in_blobdir() {
|
||||
let t = TestContext::new().await;
|
||||
let avatar_src = t.get_blobdir().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
File::create(&avatar_src)
|
||||
.await
|
||||
.unwrap()
|
||||
.write_all(avatar_bytes)
|
||||
.write_all(test_utils::AVATAR_900x900_BYTES)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
183
src/chat.rs
183
src/chat.rs
@@ -7,7 +7,6 @@ use std::time::{Duration, SystemTime};
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
@@ -171,9 +170,20 @@ impl ChatId {
|
||||
/// This should be used when **a user action** creates a chat 1:1, it ensures the chat
|
||||
/// exists and is unblocked and scales the [`Contact`]'s origin.
|
||||
pub async fn create_for_contact(context: &Context, contact_id: u32) -> Result<Self> {
|
||||
ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await
|
||||
}
|
||||
|
||||
/// Same as `create_for_contact()` with an additional `create_blocked` parameter
|
||||
/// that is used in case the chat does not exist or to unblock existing chats.
|
||||
/// `create_blocked` won't block already unblocked chats again.
|
||||
pub(crate) async fn create_for_contact_with_blocked(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
create_blocked: Blocked,
|
||||
) -> Result<Self> {
|
||||
let chat_id = match ChatIdBlocked::lookup_by_contact(context, contact_id).await? {
|
||||
Some(chat) => {
|
||||
if chat.blocked != Blocked::Not {
|
||||
if create_blocked == Blocked::Not && chat.blocked != Blocked::Not {
|
||||
chat.id.unblock(context).await?;
|
||||
}
|
||||
chat.id
|
||||
@@ -182,7 +192,10 @@ impl ChatId {
|
||||
if Contact::real_exists_by_id(context, contact_id).await?
|
||||
|| contact_id == DC_CONTACT_ID_SELF
|
||||
{
|
||||
let chat_id = ChatId::get_for_contact(context, contact_id).await?;
|
||||
let chat_id =
|
||||
ChatIdBlocked::get_for_contact(context, contact_id, create_blocked)
|
||||
.await
|
||||
.map(|chat| chat.id)?;
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat).await?;
|
||||
chat_id
|
||||
} else {
|
||||
@@ -252,7 +265,7 @@ impl ChatId {
|
||||
/// Updates chat blocked status.
|
||||
///
|
||||
/// Returns true if the value was modified.
|
||||
async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
|
||||
pub(crate) async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
|
||||
if self.is_special() {
|
||||
bail!("ignoring setting of Block-status for {}", self);
|
||||
}
|
||||
@@ -290,7 +303,7 @@ impl ChatId {
|
||||
self.delete(context).await?;
|
||||
}
|
||||
Chattype::Mailinglist => {
|
||||
if self.set_blocked(context, Blocked::Manually).await? {
|
||||
if self.set_blocked(context, Blocked::Yes).await? {
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
}
|
||||
}
|
||||
@@ -383,7 +396,7 @@ impl ChatId {
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
|
||||
// make sure, the receivers will get all keys
|
||||
reset_gossiped_timestamp(context, self).await?;
|
||||
self.reset_gossiped_timestamp(context).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -843,6 +856,48 @@ impl ChatId {
|
||||
pub fn to_u32(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub(crate) async fn reset_gossiped_timestamp(self, context: &Context) -> Result<()> {
|
||||
self.set_gossiped_timestamp(context, 0).await
|
||||
}
|
||||
|
||||
/// Get timestamp of the last gossip sent in the chat.
|
||||
/// Zero return value means that gossip was never sent.
|
||||
pub async fn get_gossiped_timestamp(self, context: &Context) -> Result<i64> {
|
||||
let timestamp: Option<i64> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT gossiped_timestamp FROM chats WHERE id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
.await?;
|
||||
Ok(timestamp.unwrap_or_default())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_gossiped_timestamp(
|
||||
self,
|
||||
context: &Context,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!self.is_special(),
|
||||
"can not set gossiped timestamp for special chats"
|
||||
);
|
||||
info!(
|
||||
context,
|
||||
"set gossiped_timestamp for chat {} to {}.", self, timestamp,
|
||||
);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
|
||||
paramsv![timestamp, self],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ChatId {
|
||||
@@ -1043,10 +1098,6 @@ impl Chat {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn get_gossiped_timestamp(&self, context: &Context) -> Result<i64> {
|
||||
get_gossiped_timestamp(context, self.id).await
|
||||
}
|
||||
|
||||
pub async fn get_color(&self, context: &Context) -> Result<u32> {
|
||||
let mut color = 0;
|
||||
|
||||
@@ -1079,7 +1130,7 @@ impl Chat {
|
||||
name: self.name.clone(),
|
||||
archived: self.visibility == ChatVisibility::Archived,
|
||||
param: self.param.to_string(),
|
||||
gossiped_timestamp: self.get_gossiped_timestamp(context).await?,
|
||||
gossiped_timestamp: self.id.get_gossiped_timestamp(context).await?,
|
||||
is_sending_locations: self.is_sending_locations,
|
||||
color: self.get_color(context).await?,
|
||||
profile_image: self
|
||||
@@ -1191,6 +1242,10 @@ impl Chat {
|
||||
msg.param.set_int(Param::AttachGroupImage, 1);
|
||||
self.param.remove(Param::Unpromoted);
|
||||
self.update_param(context).await?;
|
||||
// send_sync_msg() is called (usually) a moment later at Job::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?;
|
||||
}
|
||||
|
||||
// reset encrypt error state eg. for forwarding
|
||||
@@ -2211,7 +2266,6 @@ pub async fn create_group_chat(
|
||||
let chat_name = improve_single_line_input(chat_name);
|
||||
ensure!(!chat_name.is_empty(), "Invalid chat name");
|
||||
|
||||
let draft_txt = stock_str::new_group_draft(context, &chat_name).await;
|
||||
let grpid = dc_create_id();
|
||||
|
||||
let row_id = context
|
||||
@@ -2232,9 +2286,6 @@ pub async fn create_group_chat(
|
||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||
if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await? {
|
||||
add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF).await?;
|
||||
let mut draft_msg = Message::new(Viewtype::Text);
|
||||
draft_msg.set_text(Some(draft_txt));
|
||||
chat_id.set_draft_raw(context, &mut draft_msg).await?;
|
||||
}
|
||||
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
@@ -2353,7 +2404,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let mut msg = Message::default();
|
||||
|
||||
reset_gossiped_timestamp(context, chat_id).await?;
|
||||
chat_id.reset_gossiped_timestamp(context).await?;
|
||||
|
||||
/*this also makes sure, not contacts are added to special or normal chats*/
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
@@ -2383,6 +2434,8 @@ 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 self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
@@ -2433,45 +2486,6 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) async fn reset_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result<()> {
|
||||
set_gossiped_timestamp(context, chat_id, 0).await
|
||||
}
|
||||
|
||||
/// Get timestamp of the last gossip sent in the chat.
|
||||
/// Zero return value means that gossip was never sent.
|
||||
pub async fn get_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result<i64> {
|
||||
let timestamp: Option<i64> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT gossiped_timestamp FROM chats WHERE id=?;",
|
||||
paramsv![chat_id],
|
||||
)
|
||||
.await?;
|
||||
Ok(timestamp.unwrap_or_default())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_gossiped_timestamp(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
ensure!(!chat_id.is_special(), "can not add member to special chats");
|
||||
info!(
|
||||
context,
|
||||
"set gossiped_timestamp for chat #{} to {}.", chat_id, timestamp,
|
||||
);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
|
||||
paramsv![timestamp, chat_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<bool> {
|
||||
// versions before 12/2019 already allowed to set selfavatar, however, it was never sent to others.
|
||||
// to avoid sending out previously set selfavatars unexpectedly we added this additional check.
|
||||
@@ -2802,7 +2816,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
.query_map(
|
||||
format!(
|
||||
"SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
|
||||
msg_ids.iter().map(|_| "?").join(",")
|
||||
msg_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
|
||||
),
|
||||
rusqlite::params_from_iter(msg_ids),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
@@ -3236,7 +3250,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_self_talk() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = &t.get_self_chat().await;
|
||||
assert_eq!(DC_CONTACT_ID_SELF, 1);
|
||||
assert!(!chat.id.is_special());
|
||||
@@ -3246,6 +3260,23 @@ mod tests {
|
||||
assert!(chat.can_send(&t).await?);
|
||||
assert_eq!(chat.name, stock_str::saved_messages(&t).await);
|
||||
assert!(chat.get_profile_image(&t).await?.is_some());
|
||||
|
||||
let msg_id = send_text_msg(&t, chat.id, "foo self".to_string()).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.from_id, DC_CONTACT_ID_SELF);
|
||||
assert_eq!(msg.to_id, DC_CONTACT_ID_SELF);
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
let sent_msg = t.pop_sent_msg().await;
|
||||
let t2 = TestContext::new_alice().await;
|
||||
t2.recv_msg(&sent_msg).await;
|
||||
let chat = &t2.get_self_chat().await;
|
||||
let msg = t2.get_last_msg_in(chat.id).await;
|
||||
assert_eq!(msg.text, Some("foo self".to_string()));
|
||||
assert_eq!(msg.from_id, DC_CONTACT_ID_SELF);
|
||||
assert_eq!(msg.to_id, DC_CONTACT_ID_SELF);
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3854,7 +3885,7 @@ mod tests {
|
||||
|
||||
// create contact, then blocked chat
|
||||
let contact_id = Contact::create(&ctx, "", "claire@foo.de").await.unwrap();
|
||||
let chat_id = ChatIdBlocked::get_for_contact(&ctx, contact_id, Blocked::Manually)
|
||||
let chat_id = ChatIdBlocked::get_for_contact(&ctx, contact_id, Blocked::Yes)
|
||||
.await
|
||||
.unwrap()
|
||||
.id;
|
||||
@@ -3863,7 +3894,7 @@ mod tests {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(chat_id, chat2.id);
|
||||
assert_eq!(chat2.blocked, Blocked::Manually);
|
||||
assert_eq!(chat2.blocked, Blocked::Yes);
|
||||
|
||||
// test nonexistent contact
|
||||
let found = ChatId::lookup_by_contact(&ctx, 1234).await.unwrap();
|
||||
@@ -4373,4 +4404,38 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_for_contact_with_blocked() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::ManuallyCreated).await?;
|
||||
|
||||
// create a blocked chat
|
||||
let chat_id_orig =
|
||||
ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?;
|
||||
assert!(!chat_id_orig.is_special());
|
||||
let chat = Chat::load_from_db(&t, chat_id_orig).await?;
|
||||
assert_eq!(chat.blocked, Blocked::Yes);
|
||||
|
||||
// repeating the call, the same chat must still be blocked
|
||||
let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?;
|
||||
assert_eq!(chat_id, chat_id_orig);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.blocked, Blocked::Yes);
|
||||
|
||||
// already created chats are unblocked if requested
|
||||
let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Not).await?;
|
||||
assert_eq!(chat_id, chat_id_orig);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.blocked, Blocked::Not);
|
||||
|
||||
// however, already created chats are not re-blocked
|
||||
let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?;
|
||||
assert_eq!(chat_id, chat_id_orig);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.blocked, Blocked::Not);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM chats WHERE blocked!=? AND archived=?;",
|
||||
paramsv![Blocked::Manually, ChatVisibility::Archived],
|
||||
paramsv![Blocked::Yes, ChatVisibility::Archived],
|
||||
)
|
||||
.await?;
|
||||
Ok(count)
|
||||
@@ -399,10 +399,20 @@ mod tests {
|
||||
assert_eq!(chats.get_chat_id(1), chat_id2);
|
||||
assert_eq!(chats.get_chat_id(2), chat_id1);
|
||||
|
||||
// drafts are sorted to the top
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
chat_id2.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
// New drafts are sorted to the top
|
||||
// We have to set a draft on the other two messages, too, as
|
||||
// chat timestamps are only exact to the second and sorting by timestamp
|
||||
// would not work.
|
||||
// Message timestamps are "smeared" and unique, so we don't have this problem
|
||||
// if we have any message (can be a draft) in all chats.
|
||||
// Instead of setting drafts for chat_id1 and chat_id3, we could also sleep
|
||||
// 2s here.
|
||||
for chat_id in &[chat_id1, chat_id3, chat_id2] {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
}
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.get_chat_id(0), chat_id2);
|
||||
|
||||
|
||||
@@ -175,6 +175,11 @@ pub enum Config {
|
||||
/// 0 = no limit.
|
||||
#[strum(props(default = "0"))]
|
||||
DownloadLimit,
|
||||
|
||||
/// Send sync messages, requires `BccSelf` to be set as well.
|
||||
/// In a future versions, this switch may be removed.
|
||||
#[strum(props(default = "0"))]
|
||||
SendSyncMsgs,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -279,13 +284,13 @@ impl Context {
|
||||
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
|
||||
blob.recode_to_avatar_size(self).await?;
|
||||
self.sql.set_raw_config(key, Some(blob.as_name())).await?;
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
self.sql.set_raw_config(key, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
self.emit_event(EventType::SelfavatarChanged);
|
||||
Ok(())
|
||||
}
|
||||
Config::Selfstatus => {
|
||||
let def = stock_str::status_line(self).await;
|
||||
|
||||
@@ -8,7 +8,6 @@ mod server_params;
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::prelude::*;
|
||||
use async_std::task;
|
||||
use itertools::Itertools;
|
||||
use job::Action;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
@@ -290,12 +289,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
port: param.imap.port,
|
||||
socket: param.imap.security,
|
||||
username: param.imap.user.clone(),
|
||||
strict_tls: match param.imap.certificate_checks {
|
||||
CertificateChecks::Automatic => None,
|
||||
CertificateChecks::Strict => Some(true),
|
||||
CertificateChecks::AcceptInvalidCertificates2
|
||||
| CertificateChecks::AcceptInvalidCertificates => Some(false),
|
||||
},
|
||||
strict_tls: None,
|
||||
})
|
||||
}
|
||||
if !servers
|
||||
@@ -308,14 +302,24 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
port: param.smtp.port,
|
||||
socket: param.smtp.security,
|
||||
username: param.smtp.user.clone(),
|
||||
strict_tls: match param.smtp.certificate_checks {
|
||||
CertificateChecks::Automatic => None,
|
||||
CertificateChecks::Strict => Some(true),
|
||||
CertificateChecks::AcceptInvalidCertificates2
|
||||
| CertificateChecks::AcceptInvalidCertificates => Some(false),
|
||||
},
|
||||
strict_tls: None,
|
||||
})
|
||||
}
|
||||
|
||||
// respect certificate setting from function parameters
|
||||
for mut server in &mut servers {
|
||||
let certificate_checks = match server.protocol {
|
||||
Protocol::Imap => param.imap.certificate_checks,
|
||||
Protocol::Smtp => param.smtp.certificate_checks,
|
||||
};
|
||||
server.strict_tls = match certificate_checks {
|
||||
CertificateChecks::AcceptInvalidCertificates
|
||||
| CertificateChecks::AcceptInvalidCertificates2 => Some(false),
|
||||
CertificateChecks::Strict => Some(true),
|
||||
CertificateChecks::Automatic => server.strict_tls,
|
||||
};
|
||||
}
|
||||
|
||||
let servers = expand_param_vector(servers, ¶m.addr, ¶m_domain);
|
||||
|
||||
progress!(ctx, 550);
|
||||
@@ -677,7 +681,11 @@ async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationE
|
||||
return first_err.msg.to_string();
|
||||
}
|
||||
|
||||
errors.iter().map(|e| e.to_string()).join("\n\n")
|
||||
errors
|
||||
.iter()
|
||||
.map(|e| e.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n")
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
@@ -24,7 +24,7 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
|
||||
#[repr(i8)]
|
||||
pub enum Blocked {
|
||||
Not = 0,
|
||||
Manually = 1,
|
||||
Yes = 1,
|
||||
Request = 2,
|
||||
}
|
||||
|
||||
@@ -385,7 +385,7 @@ mod tests {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(Blocked::Not, Blocked::default());
|
||||
assert_eq!(Blocked::Not, Blocked::from_i32(0).unwrap());
|
||||
assert_eq!(Blocked::Manually, Blocked::from_i32(1).unwrap());
|
||||
assert_eq!(Blocked::Yes, Blocked::from_i32(1).unwrap());
|
||||
assert_eq!(Blocked::Request, Blocked::from_i32(2).unwrap());
|
||||
}
|
||||
|
||||
|
||||
166
src/contact.rs
166
src/contact.rs
@@ -2,10 +2,9 @@
|
||||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::path::PathBuf;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
@@ -67,6 +66,9 @@ pub struct Contact {
|
||||
/// Blocked state. Use dc_contact_is_blocked to access this field.
|
||||
pub blocked: bool,
|
||||
|
||||
/// Time when the contact was seen last time, Unix time in seconds.
|
||||
last_seen: i64,
|
||||
|
||||
/// The origin/source of the contact.
|
||||
pub origin: Origin,
|
||||
|
||||
@@ -185,7 +187,8 @@ impl Contact {
|
||||
let mut contact = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
|
||||
c.authname, c.param, c.status
|
||||
FROM contacts c
|
||||
WHERE c.id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
@@ -194,15 +197,17 @@ impl Contact {
|
||||
let addr: String = row.get(1)?;
|
||||
let origin: Origin = row.get(2)?;
|
||||
let blocked: Option<bool> = row.get(3)?;
|
||||
let authname: String = row.get(4)?;
|
||||
let param: String = row.get(5)?;
|
||||
let status: Option<String> = row.get(6)?;
|
||||
let last_seen: i64 = row.get(4)?;
|
||||
let authname: String = row.get(5)?;
|
||||
let param: String = row.get(6)?;
|
||||
let status: Option<String> = row.get(7)?;
|
||||
let contact = Self {
|
||||
id: contact_id,
|
||||
name,
|
||||
authname,
|
||||
addr,
|
||||
blocked: blocked.unwrap_or_default(),
|
||||
last_seen,
|
||||
origin,
|
||||
param: param.parse().unwrap_or_default(),
|
||||
status: status.unwrap_or_default(),
|
||||
@@ -234,6 +239,11 @@ impl Contact {
|
||||
self.blocked
|
||||
}
|
||||
|
||||
/// Returns last seen timestamp.
|
||||
pub fn last_seen(&self) -> i64 {
|
||||
self.last_seen
|
||||
}
|
||||
|
||||
/// Check if a contact is blocked.
|
||||
pub async fn is_blocked_load(context: &Context, id: u32) -> Result<bool> {
|
||||
let blocked = Self::load_from_db(context, id).await?.blocked;
|
||||
@@ -711,7 +721,7 @@ impl Contact {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT name, grpid FROM chats WHERE type=? AND blocked=?;",
|
||||
paramsv![Chattype::Mailinglist, Blocked::Manually],
|
||||
paramsv![Chattype::Mailinglist, Blocked::Yes],
|
||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
@@ -1289,6 +1299,27 @@ pub(crate) async fn set_status(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates last seen timestamp of the contact if it is earlier than the given `timestamp`.
|
||||
pub(crate) async fn update_last_seen(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
|
||||
"Can not update special contact last seen timestamp"
|
||||
);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2",
|
||||
paramsv![timestamp, contact_id],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Normalize a name.
|
||||
///
|
||||
/// - Remove quotes (come from some bad MUA implementations)
|
||||
@@ -1339,12 +1370,11 @@ fn cat_fingerprint(
|
||||
impl Context {
|
||||
/// determine whether the specified addr maps to the/a self addr
|
||||
pub async fn is_self_addr(&self, addr: &str) -> Result<bool> {
|
||||
let self_addr = self
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.ok_or_else(|| format_err!("Not configured"))?;
|
||||
|
||||
Ok(addr_cmp(self_addr, addr))
|
||||
if let Some(self_addr) = self.get_config(Config::ConfiguredAddr).await? {
|
||||
Ok(addr_cmp(self_addr, addr))
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1357,26 +1387,28 @@ pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
|
||||
|
||||
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
||||
book.lines()
|
||||
.collect::<Vec<&str>>()
|
||||
.chunks(2)
|
||||
.into_iter()
|
||||
.filter_map(|mut chunk| {
|
||||
let name = chunk.next().unwrap();
|
||||
let addr = match chunk.next() {
|
||||
Some(a) => a,
|
||||
None => return None,
|
||||
};
|
||||
Some((name, addr))
|
||||
.filter_map(|chunk| {
|
||||
let name = chunk.get(0)?;
|
||||
let addr = chunk.get(1)?;
|
||||
Some((*name, *addr))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use async_std::fs::File;
|
||||
use async_std::io::WriteExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::chat::send_text_msg;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::{self, TestContext};
|
||||
|
||||
#[test]
|
||||
fn test_may_be_valid_addr() {
|
||||
@@ -1491,7 +1523,7 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_is_self_addr() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
assert!(t.is_self_addr("me@me.org").await.is_err());
|
||||
assert_eq!(t.is_self_addr("me@me.org").await?, false);
|
||||
|
||||
let addr = t.configure_alice().await;
|
||||
assert_eq!(t.is_self_addr("me@me.org").await?, false);
|
||||
@@ -1972,4 +2004,94 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that DC_EVENT_SELFAVATAR_CHANGED is emitted on avatar changes.
|
||||
#[async_std::test]
|
||||
async fn test_selfavatar_changed_event() -> Result<()> {
|
||||
// Alice has two devices.
|
||||
let alice1 = TestContext::new_alice().await;
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
|
||||
// Bob has one device.
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
assert_eq!(alice1.get_config(Config::Selfavatar).await?, None);
|
||||
|
||||
let avatar_src = alice1.get_blobdir().join("avatar.png");
|
||||
File::create(&avatar_src)
|
||||
.await?
|
||||
.write_all(test_utils::AVATAR_900x900_BYTES)
|
||||
.await?;
|
||||
|
||||
alice1
|
||||
.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await?;
|
||||
|
||||
alice1
|
||||
.evtracker
|
||||
.get_matching(|e| e == EventType::SelfavatarChanged)
|
||||
.await;
|
||||
|
||||
// Bob sends a message so that Alice can encrypt to him.
|
||||
let chat = bob
|
||||
.create_chat_with_contact("Alice", "alice@example.com")
|
||||
.await;
|
||||
|
||||
send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
alice1.recv_msg(&sent_msg).await;
|
||||
alice2.recv_msg(&sent_msg).await;
|
||||
|
||||
// Alice sends a message.
|
||||
let alice1_chat_id = alice1.get_last_msg().await.chat_id;
|
||||
alice1_chat_id.accept(&alice1).await?;
|
||||
send_text_msg(&alice1, alice1_chat_id, "Hello".to_string()).await?;
|
||||
let sent_msg = alice1.pop_sent_msg().await;
|
||||
|
||||
// The message is encrypted.
|
||||
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
|
||||
assert!(message.get_showpadlock());
|
||||
|
||||
// Alice's second device receives a copy of the outgoing message.
|
||||
alice2.recv_msg(&sent_msg).await;
|
||||
|
||||
// Alice's second device applies the selfavatar.
|
||||
assert!(alice2.get_config(Config::Selfavatar).await?.is_some());
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|e| e == EventType::SelfavatarChanged)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_last_seen() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
|
||||
.await?;
|
||||
let contact = Contact::load_from_db(&alice, contact_id).await?;
|
||||
assert_eq!(contact.last_seen(), 0);
|
||||
|
||||
let mime = br#"Subject: Hello
|
||||
Message-ID: message@example.net
|
||||
To: Alice <alice@example.com>
|
||||
From: Bob <bob@example.net>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
Chat-Version: 1.0
|
||||
Date: Sun, 22 Mar 2020 22:37:55 +0000
|
||||
|
||||
Hi."#;
|
||||
dc_receive_imf(&alice, mime, "Inbox", 1, false).await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
let timestamp = msg.get_timestamp();
|
||||
assert!(timestamp > 0);
|
||||
let contact = Contact::load_from_db(&alice, contact_id).await?;
|
||||
assert_eq!(contact.last_seen(), timestamp);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,11 @@ pub struct InnerContext {
|
||||
pub(crate) id: u32,
|
||||
|
||||
creation_time: SystemTime,
|
||||
|
||||
/// The text of the last error logged and emitted as an event.
|
||||
/// If the ui wants to display an error after a failure,
|
||||
/// `last_error` should be used to avoid races with the event thread.
|
||||
pub(crate) last_error: RwLock<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -147,6 +152,7 @@ impl Context {
|
||||
quota: RwLock::new(None),
|
||||
creation_time: std::time::SystemTime::now(),
|
||||
last_full_folder_scan: Mutex::new(None),
|
||||
last_error: RwLock::new("".to_string()),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
@@ -303,6 +309,7 @@ impl Context {
|
||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await?;
|
||||
let send_sync_msgs = self.get_config_int(Config::SendSyncMsgs).await?;
|
||||
|
||||
let prv_key_cnt = self
|
||||
.sql
|
||||
@@ -392,6 +399,7 @@ impl Context {
|
||||
self.get_config_int(Config::KeyGenType).await?.to_string(),
|
||||
);
|
||||
res.insert("bcc_self", bcc_self.to_string());
|
||||
res.insert("send_sync_msgs", send_sync_msgs.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);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -84,9 +84,9 @@ pub(crate) fn dc_gm2local_offset() -> i64 {
|
||||
// but at max `MAX_SECONDS_TO_LEND_FROM_FUTURE`
|
||||
const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
|
||||
|
||||
// returns the currently smeared timestamp,
|
||||
// may be used to check if call to dc_create_smeared_timestamp() is needed or not.
|
||||
// the returned timestamp MUST NOT be used to be sent out or saved in the database!
|
||||
/// Returns the current smeared timestamp,
|
||||
///
|
||||
/// The returned timestamp MUST NOT be sent out.
|
||||
pub(crate) async fn dc_smeared_time(context: &Context) -> i64 {
|
||||
let mut now = time();
|
||||
let ts = *context.last_smeared_timestamp.read().await;
|
||||
@@ -97,7 +97,7 @@ pub(crate) async fn dc_smeared_time(context: &Context) -> i64 {
|
||||
now
|
||||
}
|
||||
|
||||
// returns a timestamp that is guaranteed to be unique.
|
||||
/// Returns a timestamp that is guaranteed to be unique.
|
||||
pub(crate) async fn dc_create_smeared_timestamp(context: &Context) -> i64 {
|
||||
let now = time();
|
||||
let mut ret = now;
|
||||
@@ -835,8 +835,8 @@ mod tests {
|
||||
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
|
||||
}
|
||||
|
||||
use crate::chat;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::{chat, test_utils};
|
||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
||||
use proptest::prelude::*;
|
||||
|
||||
@@ -844,22 +844,18 @@ mod tests {
|
||||
#[test]
|
||||
fn test_dc_truncate(
|
||||
buf: String,
|
||||
approx_chars in 0..10000usize
|
||||
approx_chars in 0..100usize
|
||||
) {
|
||||
let res = dc_truncate(&buf, approx_chars);
|
||||
let el_len = 5;
|
||||
let l = res.chars().count();
|
||||
if approx_chars > 0 {
|
||||
assert!(
|
||||
l <= approx_chars + el_len,
|
||||
"buf: '{}' - res: '{}' - len {}, approx {}",
|
||||
&buf, &res, res.len(), approx_chars
|
||||
);
|
||||
} else {
|
||||
assert_eq!(&res, &buf);
|
||||
}
|
||||
assert!(
|
||||
l <= approx_chars + el_len,
|
||||
"buf: '{}' - res: '{}' - len {}, approx {}",
|
||||
&buf, &res, res.len(), approx_chars
|
||||
);
|
||||
|
||||
if approx_chars > 0 && buf.chars().count() > approx_chars + el_len {
|
||||
if buf.chars().count() > approx_chars + el_len {
|
||||
let l = res.len();
|
||||
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
|
||||
}
|
||||
@@ -986,8 +982,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_filemeta() {
|
||||
let data = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
let (w, h) = dc_get_filemeta(data).unwrap();
|
||||
let (w, h) = dc_get_filemeta(test_utils::AVATAR_900x900_BYTES).unwrap();
|
||||
assert_eq!(w, 900);
|
||||
assert_eq!(h, 900);
|
||||
|
||||
|
||||
@@ -227,6 +227,7 @@ mod tests {
|
||||
use crate::chat::send_msg;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::dc_receive_imf::dc_receive_imf_inner;
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::test_utils::TestContext;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
@@ -343,4 +344,40 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_partial_download_and_ephemeral() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = t
|
||||
.create_chat_with_contact("bob", "bob@example.org")
|
||||
.await
|
||||
.id;
|
||||
chat_id
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
|
||||
// download message from bob partially, this must not change the ephemeral timer
|
||||
dc_receive_imf_inner(
|
||||
&t,
|
||||
b"From: Bob <bob@example.org>\n\
|
||||
To: Alice <alice@example.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: subject\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\
|
||||
Content-Type: text/plain",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
Some(100000),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(&t).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,4 +323,7 @@ pub enum EventType {
|
||||
/// dc_get_connectivity_html() for details.
|
||||
#[strum(props(id = "2100"))]
|
||||
ConnectivityChanged,
|
||||
|
||||
#[strum(props(id = "2110"))]
|
||||
SelfavatarChanged,
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
//! # List of email headers.
|
||||
|
||||
use crate::strum::AsStaticRef;
|
||||
use mailparse::{MailHeader, MailHeaderMap};
|
||||
|
||||
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
|
||||
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, IntoStaticStr)]
|
||||
#[strum(serialize_all = "kebab_case")]
|
||||
pub enum HeaderDef {
|
||||
MessageId,
|
||||
@@ -67,9 +66,9 @@ pub enum HeaderDef {
|
||||
}
|
||||
|
||||
impl HeaderDef {
|
||||
/// Returns the corresponding Event id.
|
||||
/// Returns the corresponding header string.
|
||||
pub fn get_headername(&self) -> &'static str {
|
||||
self.as_static()
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -361,6 +361,7 @@ impl Imap {
|
||||
|
||||
let lock = context.wrong_pw_warning_mutex.lock().await;
|
||||
if self.login_failed_once
|
||||
&& err.to_string().to_lowercase().contains("authentication")
|
||||
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
|
||||
{
|
||||
if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await {
|
||||
|
||||
45
src/job.rs
45
src/job.rs
@@ -8,7 +8,6 @@ use std::future::Future;
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Error, Result};
|
||||
use async_smtp::smtp::response::{Category, Code, Detail};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
@@ -433,6 +432,13 @@ impl Job {
|
||||
}
|
||||
// now also delete the generated file
|
||||
dc_delete_file(context, filename).await;
|
||||
|
||||
// finally, create another send-job if there are items to be synced.
|
||||
// triggering sync-job after msg-send-job guarantees, the recipient has grpid etc.
|
||||
// once the sync message arrives.
|
||||
// if there are no items to sync, this function returns fast.
|
||||
context.send_sync_msg().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
@@ -833,7 +839,7 @@ pub async fn kill_action(context: &Context, action: Action) -> Result<()> {
|
||||
async fn kill_ids(context: &Context, job_ids: &[u32]) -> Result<()> {
|
||||
let q = format!(
|
||||
"DELETE FROM jobs WHERE id IN({})",
|
||||
job_ids.iter().map(|_| "?").join(",")
|
||||
job_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
|
||||
);
|
||||
context
|
||||
.sql
|
||||
@@ -984,7 +990,7 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
|
||||
}
|
||||
|
||||
if rendered_msg.is_gossiped {
|
||||
chat::set_gossiped_timestamp(context, msg.chat_id, time()).await?;
|
||||
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
|
||||
}
|
||||
|
||||
if 0 != rendered_msg.last_added_location_id {
|
||||
@@ -1001,6 +1007,12 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
|
||||
if let Err(err) = context.delete_sync_ids(sync_ids).await {
|
||||
error!(context, "Failed to delete sync ids: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if attach_selfavatar {
|
||||
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
|
||||
error!(context, "Failed to set selfavatar timestamp: {:?}", err);
|
||||
@@ -1084,7 +1096,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
|
||||
"{} thread increases job {} tries to {}", &connection, job, tries
|
||||
);
|
||||
job.tries = tries;
|
||||
let time_offset = get_backoff_time_offset(tries);
|
||||
let time_offset = get_backoff_time_offset(tries, job.action);
|
||||
job.desired_timestamp = time() + time_offset;
|
||||
info!(
|
||||
context,
|
||||
@@ -1170,15 +1182,24 @@ async fn perform_job_action(
|
||||
try_res
|
||||
}
|
||||
|
||||
fn get_backoff_time_offset(tries: u32) -> i64 {
|
||||
let n = 2_i32.pow(tries - 1) * 60;
|
||||
let mut rng = thread_rng();
|
||||
let r: i32 = rng.gen();
|
||||
let mut seconds = r % (n + 1);
|
||||
if seconds < 1 {
|
||||
seconds = 1;
|
||||
fn get_backoff_time_offset(tries: u32, action: Action) -> i64 {
|
||||
match action {
|
||||
// Just try every 10s to update the quota
|
||||
// If all retries are exhausted, a new job will be created when the quota information is needed
|
||||
Action::UpdateRecentQuota => 10,
|
||||
|
||||
_ => {
|
||||
// Exponential backoff
|
||||
let n = 2_i32.pow(tries - 1) * 60;
|
||||
let mut rng = thread_rng();
|
||||
let r: i32 = rng.gen();
|
||||
let mut seconds = r % (n + 1);
|
||||
if seconds < 1 {
|
||||
seconds = 1;
|
||||
}
|
||||
seconds as i64
|
||||
}
|
||||
}
|
||||
seconds as i64
|
||||
}
|
||||
|
||||
async fn send_mdn(context: &Context, msg: &Message) -> Result<()> {
|
||||
|
||||
@@ -68,7 +68,6 @@ pub mod key;
|
||||
mod keyring;
|
||||
pub mod location;
|
||||
mod login_param;
|
||||
pub mod lot;
|
||||
pub mod message;
|
||||
mod mimefactory;
|
||||
pub mod mimeparser;
|
||||
@@ -83,6 +82,7 @@ pub mod securejoin;
|
||||
mod simplify;
|
||||
mod smtp;
|
||||
pub mod stock_str;
|
||||
mod sync;
|
||||
mod token;
|
||||
mod update_helper;
|
||||
#[macro_use]
|
||||
|
||||
@@ -552,17 +552,20 @@ pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save(
|
||||
/// Saves given locations to the database.
|
||||
///
|
||||
/// Returns the database row ID of the location with the highest timestamp.
|
||||
pub(crate) async fn save(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
locations: &[Location],
|
||||
independent: bool,
|
||||
) -> Result<u32> {
|
||||
) -> Result<Option<u32>> {
|
||||
ensure!(!chat_id.is_special(), "Invalid chat id");
|
||||
|
||||
let mut newest_timestamp = 0;
|
||||
let mut newest_location_id = 0;
|
||||
let mut newest_location_id = None;
|
||||
|
||||
let stmt_insert = "INSERT INTO locations\
|
||||
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
|
||||
@@ -600,12 +603,12 @@ pub async fn save(
|
||||
drop(stmt_test);
|
||||
drop(stmt_insert);
|
||||
newest_timestamp = timestamp;
|
||||
newest_location_id = conn.last_insert_rowid();
|
||||
newest_location_id = Some(u32::try_from(conn.last_insert_rowid())?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(u32::try_from(newest_location_id)?)
|
||||
Ok(newest_location_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> job::Status {
|
||||
|
||||
47
src/log.rs
47
src/log.rs
@@ -1,6 +1,7 @@
|
||||
//! # Logging.
|
||||
|
||||
use crate::context::Context;
|
||||
use async_std::task::block_on;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! info {
|
||||
@@ -39,10 +40,28 @@ macro_rules! error {
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
let formatted = format!($msg, $($args),*);
|
||||
$ctx.set_last_error(&formatted);
|
||||
$ctx.emit_event($crate::EventType::Error(formatted));
|
||||
}};
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Set last error string.
|
||||
/// Implemented as blocking as used from macros in different, not always async blocks.
|
||||
pub fn set_last_error(&self, error: &str) {
|
||||
block_on(async move {
|
||||
let mut last_error = self.last_error.write().await;
|
||||
*last_error = error.to_string();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get last error string.
|
||||
pub async fn get_last_error(&self) -> String {
|
||||
let last_error = &*self.last_error.read().await;
|
||||
last_error.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LogExt<T, E>
|
||||
where
|
||||
Self: std::marker::Sized,
|
||||
@@ -134,3 +153,31 @@ impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test_utils::TestContext;
|
||||
use anyhow::Result;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_last_error() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
assert_eq!(t.get_last_error().await, "");
|
||||
|
||||
error!(t, "foo-error");
|
||||
assert_eq!(t.get_last_error().await, "foo-error");
|
||||
|
||||
warn!(t, "foo-warning");
|
||||
assert_eq!(t.get_last_error().await, "foo-error");
|
||||
|
||||
info!(t, "foo-info");
|
||||
assert_eq!(t.get_last_error().await, "foo-error");
|
||||
|
||||
error!(t, "bar-error");
|
||||
error!(t, "baz-error");
|
||||
assert_eq!(t.get_last_error().await, "baz-error");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@ use anyhow::Result;
|
||||
use async_std::io;
|
||||
use async_std::net::TcpStream;
|
||||
|
||||
use async_native_tls::Certificate;
|
||||
pub use async_smtp::ServerAddress;
|
||||
use fast_socks5::client::Socks5Stream;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
@@ -368,8 +370,18 @@ fn get_readable_flags(flags: i32) -> String {
|
||||
res
|
||||
}
|
||||
|
||||
// this certificate is missing on older android devices (eg. lg with android6 from 2017)
|
||||
// certificate downloaded from https://letsencrypt.org/certificates/
|
||||
static LETSENCRYPT_ROOT: Lazy<Certificate> = Lazy::new(|| {
|
||||
Certificate::from_der(include_bytes!(
|
||||
"../assets/root-certificates/letsencrypt/isrgrootx1.der"
|
||||
))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
pub fn dc_build_tls(strict_tls: bool) -> async_native_tls::TlsConnector {
|
||||
let tls_builder = async_native_tls::TlsConnector::new();
|
||||
let tls_builder =
|
||||
async_native_tls::TlsConnector::new().add_root_certificate(LETSENCRYPT_ROOT.clone());
|
||||
|
||||
if strict_tls {
|
||||
tls_builder
|
||||
@@ -430,4 +442,13 @@ mod tests {
|
||||
assert_eq!(param, loaded);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_build_tls() -> Result<()> {
|
||||
// we are using some additional root certificates.
|
||||
// make sure, they do not break construction of TlsConnector
|
||||
let _ = dc_build_tls(true);
|
||||
let _ = dc_build_tls(false);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
180
src/lot.rs
180
src/lot.rs
@@ -1,180 +0,0 @@
|
||||
//! # Legacy generic return values for C API.
|
||||
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
|
||||
use crate::key::Fingerprint;
|
||||
use crate::message::MessageState;
|
||||
use crate::summary::{Summary, SummaryPrefix};
|
||||
|
||||
/// An object containing a set of values.
|
||||
/// The meaning of the values is defined by the function returning the object.
|
||||
/// Lot objects are created
|
||||
/// eg. by chatlist.get_summary() or dc_msg_get_summary().
|
||||
///
|
||||
/// *Lot* is used in the meaning *heap* here.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Lot {
|
||||
pub(crate) text1_meaning: Meaning,
|
||||
pub(crate) text1: Option<String>,
|
||||
pub(crate) text2: Option<String>,
|
||||
pub(crate) timestamp: i64,
|
||||
pub(crate) state: LotState,
|
||||
pub(crate) id: u32,
|
||||
pub(crate) fingerprint: Option<Fingerprint>,
|
||||
pub(crate) invitenumber: Option<String>,
|
||||
pub(crate) auth: Option<String>,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
|
||||
)]
|
||||
pub enum Meaning {
|
||||
None = 0,
|
||||
Text1Draft = 1,
|
||||
Text1Username = 2,
|
||||
Text1Self = 3,
|
||||
}
|
||||
|
||||
impl Default for Meaning {
|
||||
fn default() -> Self {
|
||||
Meaning::None
|
||||
}
|
||||
}
|
||||
|
||||
impl Lot {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn get_text1(&self) -> Option<&str> {
|
||||
self.text1.as_deref()
|
||||
}
|
||||
|
||||
pub fn get_text2(&self) -> Option<&str> {
|
||||
self.text2.as_deref()
|
||||
}
|
||||
|
||||
pub fn get_text1_meaning(&self) -> Meaning {
|
||||
self.text1_meaning
|
||||
}
|
||||
|
||||
pub fn get_state(&self) -> LotState {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn get_timestamp(&self) -> i64 {
|
||||
self.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u32)]
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
|
||||
)]
|
||||
pub enum LotState {
|
||||
// Default
|
||||
Undefined = 0,
|
||||
|
||||
// Qr States
|
||||
/// id=contact
|
||||
QrAskVerifyContact = 200,
|
||||
|
||||
/// text1=groupname
|
||||
QrAskVerifyGroup = 202,
|
||||
|
||||
/// id=contact
|
||||
QrFprOk = 210,
|
||||
|
||||
/// id=contact
|
||||
QrFprMismatch = 220,
|
||||
|
||||
/// test1=formatted fingerprint
|
||||
QrFprWithoutAddr = 230,
|
||||
|
||||
/// text1=domain
|
||||
QrAccount = 250,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
/// id=contact
|
||||
QrAddr = 320,
|
||||
|
||||
/// text1=text
|
||||
QrText = 330,
|
||||
|
||||
/// text1=URL
|
||||
QrUrl = 332,
|
||||
|
||||
/// text1=error string
|
||||
QrError = 400,
|
||||
|
||||
QrWithdrawVerifyContact = 500,
|
||||
|
||||
/// text1=groupname
|
||||
QrWithdrawVerifyGroup = 502,
|
||||
|
||||
QrReviveVerifyContact = 510,
|
||||
|
||||
/// text1=groupname
|
||||
QrReviveVerifyGroup = 512,
|
||||
|
||||
// Message States
|
||||
MsgInFresh = 10,
|
||||
MsgInNoticed = 13,
|
||||
MsgInSeen = 16,
|
||||
MsgOutPreparing = 18,
|
||||
MsgOutDraft = 19,
|
||||
MsgOutPending = 20,
|
||||
MsgOutFailed = 24,
|
||||
MsgOutDelivered = 26,
|
||||
MsgOutMdnRcvd = 28,
|
||||
}
|
||||
|
||||
impl Default for LotState {
|
||||
fn default() -> Self {
|
||||
LotState::Undefined
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MessageState> for LotState {
|
||||
fn from(s: MessageState) -> Self {
|
||||
use MessageState::*;
|
||||
match s {
|
||||
Undefined => LotState::Undefined,
|
||||
InFresh => LotState::MsgInFresh,
|
||||
InNoticed => LotState::MsgInNoticed,
|
||||
InSeen => LotState::MsgInSeen,
|
||||
OutPreparing => LotState::MsgOutPreparing,
|
||||
OutDraft => LotState::MsgOutDraft,
|
||||
OutPending => LotState::MsgOutPending,
|
||||
OutFailed => LotState::MsgOutFailed,
|
||||
OutDelivered => LotState::MsgOutDelivered,
|
||||
OutMdnRcvd => LotState::MsgOutMdnRcvd,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Summary> for Lot {
|
||||
fn from(summary: Summary) -> Self {
|
||||
let (text1, text1_meaning) = match summary.prefix {
|
||||
None => (None, Meaning::None),
|
||||
Some(SummaryPrefix::Draft(text)) => (Some(text), Meaning::Text1Draft),
|
||||
Some(SummaryPrefix::Username(username)) => (Some(username), Meaning::Text1Username),
|
||||
Some(SummaryPrefix::Me(text)) => (Some(text), Meaning::Text1Self),
|
||||
};
|
||||
Self {
|
||||
text1_meaning,
|
||||
text1,
|
||||
text2: Some(summary.text),
|
||||
timestamp: summary.timestamp,
|
||||
state: summary.state.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::split_armored_data;
|
||||
use crate::stock_str;
|
||||
use crate::summary::{get_summarytext_by_raw, Summary};
|
||||
use crate::summary::Summary;
|
||||
|
||||
/// Message ID, including reserved IDs.
|
||||
///
|
||||
@@ -592,7 +592,7 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Returns message summary for display in the search results.
|
||||
pub async fn get_summary(&mut self, context: &Context, chat: Option<&Chat>) -> Result<Summary> {
|
||||
pub async fn get_summary(&self, context: &Context, chat: Option<&Chat>) -> Result<Summary> {
|
||||
let chat_loaded: Chat;
|
||||
let chat = if let Some(chat) = chat {
|
||||
chat
|
||||
@@ -616,18 +616,6 @@ impl Message {
|
||||
Ok(Summary::new(context, self, chat, contact.as_ref()).await)
|
||||
}
|
||||
|
||||
pub async fn get_summarytext(&self, context: &Context, approx_characters: usize) -> String {
|
||||
get_summarytext_by_raw(
|
||||
self.viewtype,
|
||||
self.text.as_ref(),
|
||||
self.is_forwarded(),
|
||||
&self.param,
|
||||
approx_characters,
|
||||
context,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
// It's a little unfortunate that the UI has to first call dc_msg_get_override_sender_name() and then if it was NULL, call
|
||||
// dc_contact_get_display_name() but this was the best solution:
|
||||
// - We could load a Contact struct from the db here to call get_display_name() instead of returning None, but then we had a db
|
||||
@@ -871,7 +859,11 @@ impl Message {
|
||||
Param::Quote,
|
||||
if text.is_empty() {
|
||||
// Use summary, similar to "Image" to avoid sending empty quote.
|
||||
quote.get_summarytext(context, 500).await
|
||||
quote
|
||||
.get_summary(context, None)
|
||||
.await?
|
||||
.truncated_text(500)
|
||||
.to_string()
|
||||
} else {
|
||||
text
|
||||
},
|
||||
|
||||
@@ -68,6 +68,13 @@ pub struct MimeFactory<'a> {
|
||||
references: String,
|
||||
req_mdn: bool,
|
||||
last_added_location_id: u32,
|
||||
|
||||
/// If the created mime-structure contains sync-items,
|
||||
/// the IDs of these items are listed here.
|
||||
/// The IDs are returned via `RenderedEmail`
|
||||
/// and must be deleted if the message is actually queued for sending.
|
||||
sync_ids_to_delete: Option<String>,
|
||||
|
||||
attach_selfavatar: bool,
|
||||
}
|
||||
|
||||
@@ -80,6 +87,12 @@ pub struct RenderedEmail {
|
||||
pub is_gossiped: bool,
|
||||
pub last_added_location_id: u32,
|
||||
|
||||
/// A comma-separated string of sync-IDs that are used by the rendered email
|
||||
/// and must be deleted once the message is actually queued for sending
|
||||
/// (deletion must be done by `delete_sync_ids()`).
|
||||
/// If the rendered email is not queued for sending, the IDs must not be deleted.
|
||||
pub sync_ids_to_delete: Option<String>,
|
||||
|
||||
/// Message ID (Message in the sense of Email)
|
||||
pub rfc724_mid: String,
|
||||
pub subject: String,
|
||||
@@ -205,6 +218,7 @@ impl<'a> MimeFactory<'a> {
|
||||
references,
|
||||
req_mdn,
|
||||
last_added_location_id: 0,
|
||||
sync_ids_to_delete: None,
|
||||
attach_selfavatar,
|
||||
};
|
||||
Ok(factory)
|
||||
@@ -249,6 +263,7 @@ impl<'a> MimeFactory<'a> {
|
||||
references: String::default(),
|
||||
req_mdn: false,
|
||||
last_added_location_id: 0,
|
||||
sync_ids_to_delete: None,
|
||||
attach_selfavatar: false,
|
||||
};
|
||||
|
||||
@@ -346,7 +361,7 @@ impl<'a> MimeFactory<'a> {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat } => {
|
||||
// beside key- and member-changes, force re-gossip every 48 hours
|
||||
let gossiped_timestamp = chat.get_gossiped_timestamp(context).await?;
|
||||
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
|
||||
if time() > gossiped_timestamp + (2 * 24 * 60 * 60) {
|
||||
Ok(true)
|
||||
} else {
|
||||
@@ -603,12 +618,20 @@ impl<'a> MimeFactory<'a> {
|
||||
main_part
|
||||
} else {
|
||||
// Multiple parts, render as multipart.
|
||||
parts.into_iter().fold(
|
||||
PartBuilder::new()
|
||||
.message_type(MimeMultipartType::Mixed)
|
||||
.child(main_part.build()),
|
||||
|message, part| message.child(part.build()),
|
||||
)
|
||||
let part_holder = if self.msg.param.get_cmd() == SystemMessage::MultiDeviceSync {
|
||||
PartBuilder::new().header((
|
||||
"Content-Type".to_string(),
|
||||
"multipart/report; report-type=multi-device-sync".to_string(),
|
||||
))
|
||||
} else {
|
||||
PartBuilder::new().message_type(MimeMultipartType::Mixed)
|
||||
};
|
||||
|
||||
parts
|
||||
.into_iter()
|
||||
.fold(part_holder.child(main_part.build()), |message, part| {
|
||||
message.child(part.build())
|
||||
})
|
||||
};
|
||||
|
||||
let outer_message = if is_encrypted {
|
||||
@@ -729,6 +752,7 @@ impl<'a> MimeFactory<'a> {
|
||||
is_encrypted,
|
||||
is_gossiped,
|
||||
last_added_location_id,
|
||||
sync_ids_to_delete: self.sync_ids_to_delete,
|
||||
rfc724_mid,
|
||||
subject: subject_str,
|
||||
})
|
||||
@@ -873,7 +897,7 @@ impl<'a> MimeFactory<'a> {
|
||||
"ephemeral-timer-changed".to_string(),
|
||||
));
|
||||
}
|
||||
SystemMessage::LocationOnly => {
|
||||
SystemMessage::LocationOnly | SystemMessage::MultiDeviceSync => {
|
||||
// This should prevent automatic replies,
|
||||
// such as non-delivery reports.
|
||||
//
|
||||
@@ -1103,6 +1127,15 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// we do not piggyback sync-files to other self-sent-messages
|
||||
// to not risk files becoming too larger and being skipped by download-on-demand.
|
||||
if command == SystemMessage::MultiDeviceSync && self.is_e2ee_guaranteed() {
|
||||
let json = self.msg.param.get(Param::Arg).unwrap_or_default();
|
||||
let ids = self.msg.param.get(Param::Arg2).unwrap_or_default();
|
||||
parts.push(context.build_sync_part(json.to_string()).await);
|
||||
self.sync_ids_to_delete = Some(ids.to_string());
|
||||
}
|
||||
|
||||
if self.attach_selfavatar {
|
||||
match context.get_config(Config::Selfavatar).await? {
|
||||
Some(path) => match build_selfavatar_file(context, &path) {
|
||||
@@ -1157,7 +1190,11 @@ impl<'a> MimeFactory<'a> {
|
||||
{
|
||||
stock_str::encrypted_msg(context).await
|
||||
} else {
|
||||
self.msg.get_summarytext(context, 32).await
|
||||
self.msg
|
||||
.get_summary(context, None)
|
||||
.await?
|
||||
.truncated_text(32)
|
||||
.to_string()
|
||||
};
|
||||
let p2 = stock_str::read_rcpt_mail_body(context, p1).await;
|
||||
let message_text = format!("{}\r\n", format_flowed(&p2));
|
||||
@@ -1375,7 +1412,6 @@ mod tests {
|
||||
use crate::test_utils::{get_chat_msg, TestContext};
|
||||
|
||||
use async_std::fs::File;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_render_email_address() {
|
||||
|
||||
@@ -28,6 +28,7 @@ use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::simplify::simplify;
|
||||
use crate::stock_str;
|
||||
use crate::sync::SyncItems;
|
||||
|
||||
/// A parsed MIME message.
|
||||
///
|
||||
@@ -56,11 +57,14 @@ pub struct MimeMessage {
|
||||
/// this set is empty.
|
||||
pub signatures: HashSet<Fingerprint>,
|
||||
|
||||
pub gossipped_addr: HashSet<String>,
|
||||
/// The set of mail recipient addresses for which gossip headers were applied, regardless of
|
||||
/// whether they modified any peerstates.
|
||||
pub gossiped_addr: HashSet<String>,
|
||||
pub is_forwarded: bool,
|
||||
pub is_system_message: SystemMessage,
|
||||
pub location_kml: Option<location::Kml>,
|
||||
pub message_kml: Option<location::Kml>,
|
||||
pub(crate) sync_items: Option<SyncItems>,
|
||||
pub(crate) user_avatar: Option<AvatarAction>,
|
||||
pub(crate) group_avatar: Option<AvatarAction>,
|
||||
pub(crate) mdn_reports: Vec<Report>,
|
||||
@@ -124,6 +128,10 @@ pub enum SystemMessage {
|
||||
// Chat protection state changed
|
||||
ChatProtectionEnabled = 11,
|
||||
ChatProtectionDisabled = 12,
|
||||
|
||||
/// Self-sent-message that contains only json used for multi-device-sync;
|
||||
/// if possible, we attach that to other messages as for locations.
|
||||
MultiDeviceSync = 20,
|
||||
}
|
||||
|
||||
impl Default for SystemMessage {
|
||||
@@ -192,7 +200,7 @@ impl MimeMessage {
|
||||
|
||||
// Memory location for a possible decrypted message.
|
||||
let mut mail_raw = Vec::new();
|
||||
let mut gossipped_addr = Default::default();
|
||||
let mut gossiped_addr = Default::default();
|
||||
|
||||
let (mail, signatures, warn_empty_signature) =
|
||||
match e2ee::try_decrypt(context, &mail, message_time).await {
|
||||
@@ -215,7 +223,7 @@ impl MimeMessage {
|
||||
if !signatures.is_empty() {
|
||||
let gossip_headers =
|
||||
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
|
||||
gossipped_addr = update_gossip_peerstates(
|
||||
gossiped_addr = update_gossip_peerstates(
|
||||
context,
|
||||
message_time,
|
||||
&mail,
|
||||
@@ -273,12 +281,13 @@ impl MimeMessage {
|
||||
|
||||
// only non-empty if it was a valid autocrypt message
|
||||
signatures,
|
||||
gossipped_addr,
|
||||
gossiped_addr,
|
||||
is_forwarded: false,
|
||||
mdn_reports: Vec::new(),
|
||||
is_system_message: SystemMessage::Unknown,
|
||||
location_kml: None,
|
||||
message_kml: None,
|
||||
sync_items: None,
|
||||
user_avatar: None,
|
||||
group_avatar: None,
|
||||
failure_report: None,
|
||||
@@ -813,6 +822,12 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("multi-device-sync") => {
|
||||
if let Some(second) = mail.subparts.get(1) {
|
||||
self.add_single_part_if_known(context, second, is_related)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
if let Some(first) = mail.subparts.get(0) {
|
||||
any_part_added = self
|
||||
@@ -999,7 +1014,20 @@ impl MimeMessage {
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if filename == "multi-device-sync.json" {
|
||||
let serialized = String::from_utf8_lossy(decoded_data)
|
||||
.parse()
|
||||
.unwrap_or_default();
|
||||
self.sync_items = context
|
||||
.parse_sync_items(serialized)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!(context, "failed to parse sync data: {}", err);
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
}
|
||||
|
||||
/* we have a regular file attachment,
|
||||
write decoded data to new blob object */
|
||||
|
||||
@@ -1354,6 +1382,9 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates.
|
||||
///
|
||||
/// Returns the set of mail recipient addresses for which valid gossip headers were found.
|
||||
async fn update_gossip_peerstates(
|
||||
context: &Context,
|
||||
message_time: i64,
|
||||
@@ -1361,42 +1392,46 @@ async fn update_gossip_peerstates(
|
||||
gossip_headers: Vec<String>,
|
||||
) -> Result<HashSet<String>> {
|
||||
// XXX split the parsing from the modification part
|
||||
let mut gossipped_addr: HashSet<String> = Default::default();
|
||||
let mut gossiped_addr: HashSet<String> = Default::default();
|
||||
|
||||
for value in &gossip_headers {
|
||||
let gossip_header = value.parse::<Aheader>();
|
||||
|
||||
if let Ok(ref header) = gossip_header {
|
||||
if get_recipients(&mail.headers)
|
||||
.iter()
|
||||
.any(|info| info.addr == header.addr.to_lowercase())
|
||||
{
|
||||
let mut peerstate = Peerstate::from_addr(context, &header.addr).await?;
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
peerstate.apply_gossip(header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
} else {
|
||||
let p = Peerstate::from_gossip(header, message_time);
|
||||
p.save_to_db(&context.sql, true).await?;
|
||||
peerstate = Some(p);
|
||||
}
|
||||
if let Some(peerstate) = peerstate {
|
||||
peerstate
|
||||
.handle_fingerprint_change(context, message_time)
|
||||
.await?;
|
||||
}
|
||||
|
||||
gossipped_addr.insert(header.addr.clone());
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring gossipped \"{}\" as the address is not in To/Cc list.", &header.addr,
|
||||
);
|
||||
let header = match value.parse::<Aheader>() {
|
||||
Ok(header) => header,
|
||||
Err(err) => {
|
||||
warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if !get_recipients(&mail.headers)
|
||||
.iter()
|
||||
.any(|info| info.addr == header.addr.to_lowercase())
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring gossiped \"{}\" as the address is not in To/Cc list.", &header.addr,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let peerstate;
|
||||
if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? {
|
||||
p.apply_gossip(&header, message_time);
|
||||
p.save_to_db(&context.sql, false).await?;
|
||||
peerstate = p;
|
||||
} else {
|
||||
let p = Peerstate::from_gossip(&header, message_time);
|
||||
p.save_to_db(&context.sql, true).await?;
|
||||
peerstate = p;
|
||||
};
|
||||
peerstate
|
||||
.handle_fingerprint_change(context, message_time)
|
||||
.await?;
|
||||
|
||||
gossiped_addr.insert(header.addr.clone());
|
||||
}
|
||||
|
||||
Ok(gossipped_addr)
|
||||
Ok(gossiped_addr)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::str;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use async_std::path::PathBuf;
|
||||
use itertools::Itertools;
|
||||
use num_traits::FromPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -182,7 +181,7 @@ impl fmt::Display for Params {
|
||||
f,
|
||||
"{}={}",
|
||||
*key as u8 as char,
|
||||
value.split('\n').join("\n\n")
|
||||
value.split('\n').collect::<Vec<&str>>().join("\n\n")
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -524,7 +524,6 @@ pub(crate) async fn deduplicate_peerstates(sql: &Sql) -> Result<()> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::alice_keypair;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_peerstate_save_to_db() {
|
||||
|
||||
@@ -4,7 +4,7 @@ mod data;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS, PROVIDER_UPDATED};
|
||||
use async_std_resolver::{config, resolver};
|
||||
use async_std_resolver::resolver_from_system_conf;
|
||||
use chrono::{NaiveDateTime, NaiveTime};
|
||||
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)]
|
||||
@@ -118,12 +118,7 @@ pub fn get_provider_by_domain(domain: &str) -> Option<&'static Provider> {
|
||||
///
|
||||
/// For security reasons, only Gmail can be configured this way.
|
||||
pub async fn get_provider_by_mx(domain: &str) -> Option<&'static Provider> {
|
||||
if let Ok(resolver) = resolver(
|
||||
config::ResolverConfig::default(),
|
||||
config::ResolverOpts::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Ok(resolver) = resolver_from_system_conf().await {
|
||||
let mut fqdn: String = domain.to_string();
|
||||
if !fqdn.ends_with('.') {
|
||||
fqdn.push('.');
|
||||
|
||||
25
src/quota.rs
25
src/quota.rs
@@ -2,7 +2,7 @@
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_imap::types::{Quota, QuotaResource};
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::chat::add_device_msg_with_importance;
|
||||
use crate::config::Config;
|
||||
@@ -43,7 +43,7 @@ pub struct QuotaInfo {
|
||||
/// set to `Err()` if the provider does not support quota or on other errors,
|
||||
/// set to `Ok()` for valid quota information.
|
||||
/// Updated by `Action::UpdateRecentQuota`
|
||||
pub(crate) recent: Result<IndexMap<String, Vec<QuotaResource>>>,
|
||||
pub(crate) recent: Result<BTreeMap<String, Vec<QuotaResource>>>,
|
||||
|
||||
/// Timestamp when structure was modified.
|
||||
pub(crate) modified: i64,
|
||||
@@ -52,8 +52,8 @@ pub struct QuotaInfo {
|
||||
async fn get_unique_quota_roots_and_usage(
|
||||
folders: Vec<String>,
|
||||
imap: &mut Imap,
|
||||
) -> Result<IndexMap<String, Vec<QuotaResource>>> {
|
||||
let mut unique_quota_roots: IndexMap<String, Vec<QuotaResource>> = IndexMap::new();
|
||||
) -> Result<BTreeMap<String, Vec<QuotaResource>>> {
|
||||
let mut unique_quota_roots: BTreeMap<String, Vec<QuotaResource>> = BTreeMap::new();
|
||||
for folder in folders {
|
||||
let (quota_roots, quotas) = &imap.get_quota_roots(&folder).await?;
|
||||
// if there are new quota roots found in this imap folder, add them to the list
|
||||
@@ -69,7 +69,7 @@ async fn get_unique_quota_roots_and_usage(
|
||||
// messages could be recieved and so the usage could have been changed
|
||||
*unique_quota_roots
|
||||
.entry(quota_root_name.clone())
|
||||
.or_insert(vec![]) = quota.resources;
|
||||
.or_insert_with(Vec::new) = quota.resources;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ async fn get_unique_quota_roots_and_usage(
|
||||
}
|
||||
|
||||
fn get_highest_usage<'t>(
|
||||
unique_quota_roots: &'t IndexMap<String, Vec<QuotaResource>>,
|
||||
unique_quota_roots: &'t BTreeMap<String, Vec<QuotaResource>>,
|
||||
) -> Result<(u64, &'t String, &QuotaResource)> {
|
||||
let mut highest: Option<(u64, &'t String, &QuotaResource)> = None;
|
||||
for (name, resources) in unique_quota_roots {
|
||||
@@ -110,12 +110,13 @@ pub fn needs_quota_warning(curr_percentage: u64, warned_at_percentage: u64) -> b
|
||||
impl Context {
|
||||
// Adds a job to update `quota.recent`
|
||||
pub(crate) async fn schedule_quota_update(&self) -> Result<()> {
|
||||
job::kill_action(self, Action::UpdateRecentQuota).await?;
|
||||
job::add(
|
||||
self,
|
||||
job::Job::new(Action::UpdateRecentQuota, 0, Params::new(), 0),
|
||||
)
|
||||
.await?;
|
||||
if !job::action_exists(self, Action::UpdateRecentQuota).await? {
|
||||
job::add(
|
||||
self,
|
||||
job::Job::new(Action::UpdateRecentQuota, 0, Params::new(), 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -346,6 +346,10 @@ impl Context {
|
||||
<body>"#
|
||||
.to_string();
|
||||
|
||||
// =============================================================================================
|
||||
// Get the states from the RwLock
|
||||
// =============================================================================================
|
||||
|
||||
let lock = self.scheduler.read().await;
|
||||
let (folders_states, smtp) = match &*lock {
|
||||
Scheduler::Running {
|
||||
@@ -380,6 +384,13 @@ impl Context {
|
||||
};
|
||||
drop(lock);
|
||||
|
||||
// =============================================================================================
|
||||
// Add e.g.
|
||||
// Incoming messages
|
||||
// - "Inbox": Connected
|
||||
// - "Sent": Connected
|
||||
// =============================================================================================
|
||||
|
||||
ret += &format!("<h3>{}</h3><ul>", stock_str::incoming_messages(self).await);
|
||||
for (folder, watch, state) in &folders_states {
|
||||
let w = self.get_config(*watch).await.ok_or_log(self);
|
||||
@@ -417,6 +428,12 @@ impl Context {
|
||||
}
|
||||
ret += "</ul>";
|
||||
|
||||
// =============================================================================================
|
||||
// Add e.g.
|
||||
// Outgoing messages
|
||||
// Your last message was sent successfully
|
||||
// =============================================================================================
|
||||
|
||||
ret += &format!(
|
||||
"<h3>{}</h3><ul><li>",
|
||||
stock_str::outgoing_messages(self).await
|
||||
@@ -427,6 +444,13 @@ impl Context {
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self).await);
|
||||
ret += "</li></ul>";
|
||||
|
||||
// =============================================================================================
|
||||
// Add e.g.
|
||||
// Storage on testrun.org
|
||||
// 1.34 GiB of 2 GiB used
|
||||
// [======67%===== ]
|
||||
// =============================================================================================
|
||||
|
||||
let domain = dc_tools::EmailAddress::new(
|
||||
&self
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
@@ -527,6 +551,8 @@ impl Context {
|
||||
}
|
||||
ret += "</ul>";
|
||||
|
||||
// =============================================================================================
|
||||
|
||||
ret += "</body></html>\n";
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
//! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol).
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::{anyhow, bail, Context as _, Error, Result};
|
||||
use async_std::channel::Receiver;
|
||||
use anyhow::{bail, Context as _, Error, Result};
|
||||
use async_std::sync::Mutex;
|
||||
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked};
|
||||
use crate::chat::{self, is_contact_in_chat, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
|
||||
use crate::constants::{Blocked, Chattype, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
|
||||
use crate::contact::{Contact, Origin, VerifiedStatus};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
@@ -30,8 +28,9 @@ use crate::token;
|
||||
mod bobstate;
|
||||
mod qrinvite;
|
||||
|
||||
use crate::token::Namespace;
|
||||
use bobstate::{BobHandshakeStage, BobState, BobStateHandle};
|
||||
use qrinvite::{QrError, QrInvite};
|
||||
use qrinvite::QrInvite;
|
||||
|
||||
pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
|
||||
|
||||
@@ -80,8 +79,8 @@ enum StartedProtocolVariant {
|
||||
SetupContact,
|
||||
/// The secure-join protocol, to join a group.
|
||||
SecureJoin {
|
||||
ongoing_receiver: Receiver<()>,
|
||||
group_id: String,
|
||||
group_name: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -109,16 +108,14 @@ impl Bob {
|
||||
*guard = None;
|
||||
}
|
||||
let variant = match invite {
|
||||
QrInvite::Group { ref grpid, .. } => {
|
||||
let receiver = context
|
||||
.alloc_ongoing()
|
||||
.await
|
||||
.map_err(|_| JoinError::OngoingRunning)?;
|
||||
StartedProtocolVariant::SecureJoin {
|
||||
ongoing_receiver: receiver,
|
||||
group_id: grpid.clone(),
|
||||
}
|
||||
}
|
||||
QrInvite::Group {
|
||||
ref grpid,
|
||||
ref name,
|
||||
..
|
||||
} => StartedProtocolVariant::SecureJoin {
|
||||
group_id: grpid.clone(),
|
||||
group_name: name.clone(),
|
||||
},
|
||||
_ => StartedProtocolVariant::SetupContact,
|
||||
};
|
||||
match BobState::start_protocol(context, invite).await {
|
||||
@@ -171,8 +168,11 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> R
|
||||
|
||||
// invitenumber will be used to allow starting the handshake,
|
||||
// auth will be used to verify the fingerprint
|
||||
let invitenumber = token::lookup_or_new(context, token::Namespace::InviteNumber, group).await;
|
||||
let auth = token::lookup_or_new(context, token::Namespace::Auth, group).await;
|
||||
let sync_token = token::lookup(context, Namespace::InviteNumber, group)
|
||||
.await?
|
||||
.is_none();
|
||||
let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, group).await;
|
||||
let auth = token::lookup_or_new(context, Namespace::Auth, group).await;
|
||||
let self_addr = match context.get_config(Config::ConfiguredAddr).await {
|
||||
Ok(Some(addr)) => addr,
|
||||
Ok(None) => {
|
||||
@@ -208,7 +208,9 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> R
|
||||
let chat = Chat::load_from_db(context, group).await?;
|
||||
let group_name = chat.get_name();
|
||||
let group_name_urlencoded = utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
|
||||
|
||||
if sync_token {
|
||||
context.sync_qr_code_tokens(Some(chat.id)).await?;
|
||||
}
|
||||
format!(
|
||||
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
|
||||
fingerprint.hex(),
|
||||
@@ -220,6 +222,9 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> R
|
||||
)
|
||||
} else {
|
||||
// parameters used: a=n=i=s=
|
||||
if sync_token {
|
||||
context.sync_qr_code_tokens(None).await?;
|
||||
}
|
||||
format!(
|
||||
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
|
||||
fingerprint.hex(),
|
||||
@@ -247,9 +252,6 @@ async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum JoinError {
|
||||
#[error("Unknown QR-code: {0}")]
|
||||
QrCode(#[from] QrError),
|
||||
|
||||
#[error("An \"ongoing\" process is already running")]
|
||||
OngoingRunning,
|
||||
|
||||
@@ -274,9 +276,7 @@ pub enum JoinError {
|
||||
/// This is the start of the process for the joiner. See the module and ffi documentation
|
||||
/// for more details.
|
||||
///
|
||||
/// When joining a group this will start an "ongoing" process and will block until the
|
||||
/// process is completed, the [`ChatId`] for the new group is not known any sooner. When
|
||||
/// verifying a contact this returns immediately.
|
||||
/// The function returns immediately and the handshake will run in background.
|
||||
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
securejoin(context, qr).await.map_err(|err| {
|
||||
warn!(context, "Fatal joiner error: {:#}", err);
|
||||
@@ -293,7 +293,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
========================================================*/
|
||||
|
||||
info!(context, "Requesting secure-join ...",);
|
||||
let qr_scan = check_qr(context, qr).await;
|
||||
let qr_scan = check_qr(context, qr).await?;
|
||||
|
||||
let invite = QrInvite::try_from(qr_scan)?;
|
||||
|
||||
@@ -307,39 +307,34 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
Ok(chat_id)
|
||||
}
|
||||
StartedProtocolVariant::SecureJoin {
|
||||
ongoing_receiver,
|
||||
group_id,
|
||||
group_name,
|
||||
} => {
|
||||
// for a group-join, wait until the protocol is finished and the group is created
|
||||
ongoing_receiver
|
||||
.recv()
|
||||
.await
|
||||
.map_err(|_| JoinError::OngoingSenderDropped)?;
|
||||
|
||||
// handle_securejoin_handshake() calls Context::stop_ongoing before the group
|
||||
// chat is created (it is created after handle_securejoin_handshake() returns by
|
||||
// dc_receive_imf()). As a hack we just wait a bit for it to appear.
|
||||
|
||||
// If the protocol is aborted by Bob, this timeout will also happen.
|
||||
let start = Instant::now();
|
||||
let chatid = loop {
|
||||
{
|
||||
match chat::get_chat_id_by_grpid(context, &group_id).await? {
|
||||
Some((chatid, _is_protected, _blocked)) => break chatid,
|
||||
None => {
|
||||
if start.elapsed() > Duration::from_secs(7) {
|
||||
context.free_ongoing().await;
|
||||
return Err(JoinError::Other(anyhow!(
|
||||
"Ongoing sender dropped (this is a bug)"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async_std::task::sleep(Duration::from_millis(50)).await;
|
||||
// for a group-join, also create the chat soon and let the verification run in background.
|
||||
// however, the group will become usable only when the protocol has finished.
|
||||
let contact_id = invite.contact_id();
|
||||
let chat_id = if let Some((chat_id, _protected, _blocked)) =
|
||||
chat::get_chat_id_by_grpid(context, &group_id).await?
|
||||
{
|
||||
chat_id.unblock(context).await?;
|
||||
chat_id
|
||||
} else {
|
||||
ChatId::create_multiuser_record(
|
||||
context,
|
||||
Chattype::Group,
|
||||
group_id,
|
||||
group_name,
|
||||
Blocked::Not,
|
||||
ProtectionStatus::Unprotected, // protection is added later as needed
|
||||
)
|
||||
.await?
|
||||
};
|
||||
context.free_ongoing().await;
|
||||
Ok(chatid)
|
||||
if !is_contact_in_chat(context, chat_id, contact_id).await? {
|
||||
chat::add_to_chat_contacts_table(context, chat_id, contact_id).await?;
|
||||
}
|
||||
let msg = stock_str::secure_join_started(context, contact_id).await;
|
||||
chat::add_info_msg(context, chat_id, msg, time()).await?;
|
||||
Ok(chat_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,13 +347,13 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
#[error("Failed sending handshake message")]
|
||||
pub struct SendMsgError(#[from] anyhow::Error);
|
||||
|
||||
async fn send_handshake_msg(
|
||||
/// Send handshake message from Alice's device;
|
||||
/// Bob's handshake messages are sent in `BobState::send_handshake_message()`.
|
||||
async fn send_alice_handshake_msg(
|
||||
context: &Context,
|
||||
contact_chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
step: &str,
|
||||
param2: &str,
|
||||
fingerprint: Option<Fingerprint>,
|
||||
grpid: &str,
|
||||
) -> Result<(), SendMsgError> {
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
@@ -367,67 +362,55 @@ async fn send_handshake_msg(
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
|
||||
if step.is_empty() {
|
||||
msg.param.remove(Param::Arg);
|
||||
} else {
|
||||
msg.param.set(Param::Arg, step);
|
||||
}
|
||||
if !param2.is_empty() {
|
||||
msg.param.set(Param::Arg2, param2);
|
||||
}
|
||||
msg.param.set(Param::Arg, step);
|
||||
if let Some(fp) = fingerprint {
|
||||
msg.param.set(Param::Arg3, fp.hex());
|
||||
}
|
||||
if !grpid.is_empty() {
|
||||
msg.param.set(Param::Arg4, grpid);
|
||||
}
|
||||
if step == "vg-request" || step == "vc-request" {
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
} else {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
}
|
||||
|
||||
chat::send_msg(context, contact_chat_id, &mut msg).await?;
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
chat::send_msg(
|
||||
context,
|
||||
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
|
||||
.await?
|
||||
.id,
|
||||
&mut msg,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> Result<u32, Error> {
|
||||
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] {
|
||||
Ok(contact_id)
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
/// Get an unblocked chat that can be used for info messages.
|
||||
async fn info_chat_id(context: &Context, contact_id: u32) -> Result<ChatId> {
|
||||
let chat_id_blocked = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not).await?;
|
||||
Ok(chat_id_blocked.id)
|
||||
}
|
||||
|
||||
async fn fingerprint_equals_sender(
|
||||
context: &Context,
|
||||
fingerprint: &Fingerprint,
|
||||
contact_chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
) -> Result<bool, Error> {
|
||||
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] {
|
||||
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
|
||||
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
|
||||
Ok(peerstate) => peerstate,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to sender peerstate for {}: {}",
|
||||
contact.get_addr(),
|
||||
err
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
let contact = Contact::load_from_db(context, contact_id).await?;
|
||||
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
|
||||
Ok(peerstate) => peerstate,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to sender peerstate for {}: {}",
|
||||
contact.get_addr(),
|
||||
err
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(peerstate) = peerstate {
|
||||
if peerstate.public_key_fingerprint.is_some()
|
||||
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
if let Some(peerstate) = peerstate {
|
||||
if peerstate.public_key_fingerprint.is_some()
|
||||
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
@@ -488,21 +471,6 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
">>>>>>>>>>>>>>>>>>>>>>>>> secure-join message \'{}\' received", step,
|
||||
);
|
||||
|
||||
let contact_chat_id = {
|
||||
let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to look up or create chat for contact {}",
|
||||
contact_id
|
||||
)
|
||||
})?;
|
||||
if chat.blocked != Blocked::Not {
|
||||
chat.id.unblock(context).await?;
|
||||
}
|
||||
chat.id
|
||||
};
|
||||
|
||||
let join_vg = step.starts_with("vg-");
|
||||
|
||||
match step.as_str() {
|
||||
@@ -531,14 +499,18 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
|
||||
inviter_progress!(context, contact_id, 300);
|
||||
|
||||
// for setup-contact, make Alice's one-to-one chat with Bob visible
|
||||
// (secure-join-information are shown in the group chat)
|
||||
if !join_vg {
|
||||
ChatId::create_for_contact(context, contact_id).await?;
|
||||
}
|
||||
|
||||
// Alice -> Bob
|
||||
send_handshake_msg(
|
||||
send_alice_handshake_msg(
|
||||
context,
|
||||
contact_chat_id,
|
||||
contact_id,
|
||||
&format!("{}-auth-required", &step[..2]),
|
||||
"",
|
||||
None,
|
||||
"",
|
||||
)
|
||||
.await?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
@@ -551,11 +523,28 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
match context.bob.state(context).await {
|
||||
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
|
||||
Some(BobHandshakeStage::Terminated(why)) => {
|
||||
could_not_establish_secure_connection(context, bobstate.chat_id(), why)
|
||||
.await?;
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
bobstate.chat_id(context).await?,
|
||||
why,
|
||||
)
|
||||
.await?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
Some(_stage) => {
|
||||
if join_vg {
|
||||
// the message reads "Alice replied, waiting for being added to the group…";
|
||||
// show it only on secure-join and not on setup-contact therefore.
|
||||
let msg = stock_str::secure_join_replies(context, contact_id).await;
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
bobstate.chat_id(context).await?,
|
||||
msg,
|
||||
time(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
joiner_progress!(context, bobstate.invite().contact_id(), 400);
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
@@ -578,7 +567,8 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
None => {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Fingerprint not provided.",
|
||||
)
|
||||
.await?;
|
||||
@@ -588,16 +578,18 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Auth not encrypted.",
|
||||
)
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await? {
|
||||
if !fingerprint_equals_sender(context, &fingerprint, contact_id).await? {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Fingerprint mismatch on inviter-side.",
|
||||
)
|
||||
.await?;
|
||||
@@ -610,7 +602,8 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
None => {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Auth not provided.",
|
||||
)
|
||||
.await?;
|
||||
@@ -618,14 +611,20 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
}
|
||||
};
|
||||
if !token::exists(context, token::Namespace::Auth, auth_0).await {
|
||||
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.")
|
||||
.await?;
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Auth invalid.",
|
||||
)
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Fingerprint mismatch on inviter-side.",
|
||||
)
|
||||
.await?;
|
||||
@@ -633,7 +632,6 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
}
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await?;
|
||||
info!(context, "Auth verified.",);
|
||||
secure_connection_established(context, contact_chat_id).await?;
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
inviter_progress!(context, contact_id, 600);
|
||||
if join_vg {
|
||||
@@ -649,6 +647,7 @@ 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?;
|
||||
if let Err(err) =
|
||||
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
|
||||
.await
|
||||
@@ -660,13 +659,17 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
}
|
||||
} else {
|
||||
// Alice -> Bob
|
||||
send_handshake_msg(
|
||||
secure_connection_established(
|
||||
context,
|
||||
contact_chat_id,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
)
|
||||
.await?;
|
||||
send_alice_handshake_msg(
|
||||
context,
|
||||
contact_id,
|
||||
"vc-contact-confirm",
|
||||
"",
|
||||
Some(fingerprint),
|
||||
"",
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -688,13 +691,23 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
match context.bob.state(context).await {
|
||||
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
|
||||
Some(BobHandshakeStage::Terminated(why)) => {
|
||||
could_not_establish_secure_connection(context, bobstate.chat_id(), why)
|
||||
.await?;
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
bobstate.chat_id(context).await?,
|
||||
why,
|
||||
)
|
||||
.await?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
Some(BobHandshakeStage::Completed) => {
|
||||
// Can only be BobHandshakeStage::Completed
|
||||
secure_connection_established(context, bobstate.chat_id()).await?;
|
||||
secure_connection_established(
|
||||
context,
|
||||
contact_id,
|
||||
bobstate.chat_id(context).await?,
|
||||
)
|
||||
.await?;
|
||||
Ok(retval)
|
||||
}
|
||||
Some(_) => {
|
||||
@@ -778,21 +791,6 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
.context("Not a Secure-Join message")?;
|
||||
info!(context, "observing secure-join message \'{}\'", step);
|
||||
|
||||
let contact_chat_id = {
|
||||
let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to look up or create chat for contact {}",
|
||||
contact_id
|
||||
)
|
||||
})?;
|
||||
if chat.blocked != Blocked::Not {
|
||||
chat.id.unblock(context).await?;
|
||||
}
|
||||
chat.id
|
||||
};
|
||||
|
||||
match step.as_str() {
|
||||
"vg-member-added"
|
||||
| "vc-contact-confirm"
|
||||
@@ -805,7 +803,8 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Message not encrypted correctly.",
|
||||
)
|
||||
.await?;
|
||||
@@ -817,7 +816,8 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
None => {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Fingerprint not provided, please update Delta Chat on all your devices.",
|
||||
)
|
||||
.await?;
|
||||
@@ -827,7 +827,8 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
format!("Fingerprint mismatch on observing {}.", step).as_ref(),
|
||||
)
|
||||
.await?;
|
||||
@@ -845,30 +846,22 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
|
||||
async fn secure_connection_established(
|
||||
context: &Context,
|
||||
contact_chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
chat_id: ChatId,
|
||||
) -> Result<(), Error> {
|
||||
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await;
|
||||
|
||||
let addr = if let Ok(ref contact) = contact {
|
||||
contact.get_addr()
|
||||
} else {
|
||||
"?"
|
||||
};
|
||||
let msg = stock_str::contact_verified(context, addr).await;
|
||||
chat::add_info_msg(context, contact_chat_id, msg, time()).await?;
|
||||
context.emit_event(EventType::ChatModified(contact_chat_id));
|
||||
info!(context, "StockMessage::ContactVerified posted to 1:1 chat");
|
||||
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let msg = stock_str::contact_verified(context, contact.get_name_n_addr()).await;
|
||||
chat::add_info_msg(context, chat_id, msg, time()).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn could_not_establish_secure_connection(
|
||||
context: &Context,
|
||||
contact_chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
chat_id: ChatId,
|
||||
details: &str,
|
||||
) -> Result<(), Error> {
|
||||
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await;
|
||||
let msg = stock_str::contact_not_verified(
|
||||
context,
|
||||
@@ -879,13 +872,11 @@ async fn could_not_establish_secure_connection(
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
chat::add_info_msg(context, contact_chat_id, &msg, time()).await?;
|
||||
error!(
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
warn!(
|
||||
context,
|
||||
"StockMessage::ContactNotVerified posted to 1:1 chat ({})", details
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -949,15 +940,19 @@ mod tests {
|
||||
|
||||
use crate::chat;
|
||||
use crate::chat::ProtectionStatus;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::events::Event;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::test_utils::TestContext;
|
||||
use std::time::Duration;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_setup_contact() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
|
||||
// Setup JoinerProgress sinks.
|
||||
let (joiner_progress_tx, joiner_progress_rx) = async_std::channel::bounded(100);
|
||||
@@ -976,6 +971,7 @@ mod tests {
|
||||
|
||||
// Step 2: Bob scans QR-code, sends vc-request
|
||||
dc_join_securejoin(&bob.ctx, &qr).await?;
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(!bob.ctx.has_ongoing().await);
|
||||
@@ -987,6 +983,7 @@ mod tests {
|
||||
|
||||
// Step 3: Alice receives vc-request, sends vc-auth-required
|
||||
alice.recv_msg(&sent).await;
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
@@ -1063,6 +1060,11 @@ mod tests {
|
||||
VerifiedStatus::BidirectVerified
|
||||
);
|
||||
|
||||
// exactly one one-to-one chat should be visible for both now
|
||||
// (check this before calling alice.create_chat() explicitly below)
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
|
||||
// Check Alice got the verified message in her 1:1 chat.
|
||||
{
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
@@ -1146,7 +1148,7 @@ mod tests {
|
||||
async fn test_setup_contact_bad_qr() {
|
||||
let bob = TestContext::new_bob().await;
|
||||
let ret = dc_join_securejoin(&bob.ctx, "not a qr code").await;
|
||||
assert!(matches!(ret, Err(JoinError::QrCode(_))));
|
||||
assert!(ret.is_err());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -1324,6 +1326,8 @@ mod tests {
|
||||
async fn test_secure_join() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
|
||||
// Setup JoinerProgress sinks.
|
||||
let (joiner_progress_tx, joiner_progress_rx) = async_std::channel::bounded(100);
|
||||
@@ -1345,15 +1349,11 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 2: Bob scans QR-code, sends vg-request; blocks on ongoing process
|
||||
let joiner = {
|
||||
let qr = qr.clone();
|
||||
let ctx = bob.ctx.clone();
|
||||
async_std::task::spawn(async move { dc_join_securejoin(&ctx, &qr).await.unwrap() })
|
||||
};
|
||||
// Step 2: Bob scans QR-code, sends vg-request
|
||||
let bob_chatid = dc_join_securejoin(&bob.ctx, &qr).await?;
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(bob.ctx.has_ongoing().await);
|
||||
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
@@ -1467,10 +1467,24 @@ mod tests {
|
||||
"vg-member-added-received"
|
||||
);
|
||||
|
||||
let bob_chatid = joiner.await;
|
||||
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
|
||||
assert!(bob_chat.is_protected());
|
||||
assert!(bob_chat.typ == Chattype::Group);
|
||||
assert!(!bob.ctx.has_ongoing().await);
|
||||
|
||||
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
|
||||
// The one-to-one chats are used internally for the hidden handshake messages,
|
||||
// however, should not be visible in the UIs.
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
|
||||
// If Bob then sends a direct message to alice, however, the one-to-one with Alice should appear.
|
||||
let bobs_chat_with_alice = bob.create_chat(&alice).await;
|
||||
let sent = bob.send_text(bobs_chat_with_alice.id, "Hello").await;
|
||||
alice.recv_msg(&sent).await;
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 2);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
//! protocol. Afterwards it must be stored in a mutex and the [`BobStateHandle`] should be
|
||||
//! used to work with the state.
|
||||
|
||||
use anyhow::{Error, Result};
|
||||
use anyhow::{bail, Error, Result};
|
||||
use async_std::sync::MutexGuard;
|
||||
|
||||
use crate::chat::{self, ChatId};
|
||||
use crate::constants::Viewtype;
|
||||
use crate::constants::{Blocked, Viewtype};
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
@@ -67,9 +67,18 @@ impl<'a> BobStateHandle<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the [`ChatId`] of the 1:1 chat with the inviter (Alice).
|
||||
pub fn chat_id(&self) -> ChatId {
|
||||
self.bobstate.chat_id
|
||||
/// Returns the [`ChatId`] of the group chat to join or the 1:1 chat with Alice.
|
||||
pub async fn chat_id(&self, context: &Context) -> Result<ChatId> {
|
||||
match self.bobstate.invite {
|
||||
QrInvite::Group { ref grpid, .. } => {
|
||||
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, &grpid).await? {
|
||||
Ok(chat_id)
|
||||
} else {
|
||||
bail!("chat not found")
|
||||
}
|
||||
}
|
||||
QrInvite::Contact { .. } => Ok(self.bobstate.chat_id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the [`QrInvite`] of the joiner process.
|
||||
@@ -185,10 +194,11 @@ impl BobState {
|
||||
context: &Context,
|
||||
invite: QrInvite,
|
||||
) -> Result<(Self, BobHandshakeStage), JoinError> {
|
||||
let chat_id = ChatId::create_for_contact(context, invite.contact_id())
|
||||
.await
|
||||
.map_err(JoinError::UnknownContact)?;
|
||||
if fingerprint_equals_sender(context, invite.fingerprint(), chat_id).await? {
|
||||
let chat_id =
|
||||
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), Blocked::Yes)
|
||||
.await
|
||||
.map_err(JoinError::UnknownContact)?;
|
||||
if fingerprint_equals_sender(context, invite.fingerprint(), invite.contact_id()).await? {
|
||||
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
|
||||
info!(context, "Taking securejoin protocol shortcut");
|
||||
let state = Self {
|
||||
@@ -297,7 +307,9 @@ impl BobState {
|
||||
self.next = SecureJoinStep::Terminated;
|
||||
return Ok(Some(BobHandshakeStage::Terminated(reason)));
|
||||
}
|
||||
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await? {
|
||||
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.invite.contact_id())
|
||||
.await?
|
||||
{
|
||||
self.next = SecureJoinStep::Terminated;
|
||||
return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch")));
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
//! Supporting code for the QR-code invite.
|
||||
//!
|
||||
//! QR-codes are decoded into a more general-purpose [`Lot`] struct normally, this struct is
|
||||
//! so general it is not even specific to QR-codes. This makes working with it rather hard,
|
||||
//! so here we have a wrapper type that specifically deals with Secure-Join QR-codes so
|
||||
//! that the Secure-Join code can have many more guarantees when dealing with this.
|
||||
//! QR-codes are decoded into a more general-purpose [`Qr`] struct normally. This makes working
|
||||
//! with it rather hard, so here we have a wrapper type that specifically deals with Secure-Join
|
||||
//! QR-codes so that the Secure-Join code can have more guarantees when dealing with this.
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Error, Result};
|
||||
|
||||
use crate::key::Fingerprint;
|
||||
use crate::lot::{Lot, LotState};
|
||||
use crate::qr::Qr;
|
||||
|
||||
/// Represents the data from a QR-code scan.
|
||||
///
|
||||
@@ -66,53 +65,38 @@ impl QrInvite {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Lot> for QrInvite {
|
||||
type Error = QrError;
|
||||
impl TryFrom<Qr> for QrInvite {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(lot: Lot) -> Result<Self, Self::Error> {
|
||||
if lot.state != LotState::QrAskVerifyContact && lot.state != LotState::QrAskVerifyGroup {
|
||||
return Err(QrError::UnsupportedProtocol);
|
||||
}
|
||||
if lot.id == 0 {
|
||||
return Err(QrError::MissingContactId);
|
||||
}
|
||||
let fingerprint = lot.fingerprint.ok_or(QrError::MissingFingerprint)?;
|
||||
let invitenumber = lot.invitenumber.ok_or(QrError::MissingInviteNumber)?;
|
||||
let authcode = lot.auth.ok_or(QrError::MissingAuthCode)?;
|
||||
match lot.state {
|
||||
LotState::QrAskVerifyContact => Ok(QrInvite::Contact {
|
||||
contact_id: lot.id,
|
||||
fn try_from(qr: Qr) -> Result<Self> {
|
||||
match qr {
|
||||
Qr::AskVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => Ok(QrInvite::Contact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}),
|
||||
LotState::QrAskVerifyGroup => Ok(QrInvite::Group {
|
||||
contact_id: lot.id,
|
||||
Qr::AskVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
name: lot.text1.ok_or(QrError::MissingGroupName)?,
|
||||
grpid: lot.text2.ok_or(QrError::MissingGroupId)?,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => Ok(QrInvite::Group {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
name: grpname,
|
||||
grpid,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}),
|
||||
_ => Err(QrError::UnsupportedProtocol),
|
||||
_ => bail!("Unsupported QR type {:?}", qr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum QrError {
|
||||
#[error("Unsupported protocol in QR-code")]
|
||||
UnsupportedProtocol,
|
||||
#[error("Missing fingerprint")]
|
||||
MissingFingerprint,
|
||||
#[error("Missing invitenumber")]
|
||||
MissingInviteNumber,
|
||||
#[error("Missing auth code")]
|
||||
MissingAuthCode,
|
||||
#[error("Missing group name")]
|
||||
MissingGroupName,
|
||||
#[error("Missing group id")]
|
||||
MissingGroupId,
|
||||
#[error("Missing contact id")]
|
||||
MissingContactId,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! # Simplify incoming plaintext.
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
// protect lines starting with `--` against being treated as a footer.
|
||||
// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B);
|
||||
// this should be invisible on most systems and there is no need to unescape it again
|
||||
@@ -158,6 +156,7 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>)
|
||||
s.strip_prefix('>')
|
||||
.map_or(*s, |u| u.strip_prefix(' ').unwrap_or(u))
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
|
||||
l_last -= 1
|
||||
@@ -204,6 +203,7 @@ fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
|
||||
s.strip_prefix('>')
|
||||
.map_or(*s, |u| u.strip_prefix(' ').unwrap_or(u))
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ use async_smtp::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||
use crate::constants::DEFAULT_MAX_SMTP_RCPT_TO;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use itertools::Itertools;
|
||||
use std::time::Duration;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -43,10 +42,14 @@ impl Smtp {
|
||||
}
|
||||
|
||||
for recipients_chunk in recipients.chunks(chunk_size).into_iter() {
|
||||
let recipients = recipients_chunk.to_vec();
|
||||
let recipients_display = recipients.iter().map(|x| x.to_string()).join(",");
|
||||
let recipients_display = recipients_chunk
|
||||
.iter()
|
||||
.map(|x| x.as_ref())
|
||||
.collect::<Vec<&str>>()
|
||||
.join(",");
|
||||
|
||||
let envelope = Envelope::new(self.from.clone(), recipients).map_err(Error::Envelope)?;
|
||||
let envelope = Envelope::new(self.from.clone(), recipients_chunk.to_vec())
|
||||
.map_err(Error::Envelope)?;
|
||||
let mail = SendableEmail::new(
|
||||
envelope,
|
||||
format!("{}", job_id), // only used for internal logging
|
||||
|
||||
@@ -487,6 +487,16 @@ paramsv![]
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 80 {
|
||||
info!(context, "[migration] v80");
|
||||
sql.execute_migration(
|
||||
r#"CREATE TABLE multi_device_sync (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
item TEXT DEFAULT '');"#,
|
||||
80,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok((
|
||||
recalc_fingerprints,
|
||||
|
||||
@@ -56,9 +56,6 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Sent with my Delta Chat Messenger: https://delta.chat"))]
|
||||
StatusLine = 13,
|
||||
|
||||
#[strum(props(fallback = "Hello, I\'ve just created the group \"%1$s\" for us."))]
|
||||
NewGroupDraft = 14,
|
||||
|
||||
#[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\"."))]
|
||||
MsgGrpName = 15,
|
||||
|
||||
@@ -276,6 +273,15 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Download maximum available until %1$s"))]
|
||||
DownloadAvailability = 100,
|
||||
|
||||
#[strum(props(fallback = "Multi Device Synchronization"))]
|
||||
SyncMsgSubject = 101,
|
||||
|
||||
#[strum(props(
|
||||
fallback = "This message is used to synchronize data between your devices.\n\n\
|
||||
👉 If you see this message in Delta Chat, please update your Delta Chat apps on all devices."
|
||||
))]
|
||||
SyncMsgBody = 102,
|
||||
|
||||
#[strum(props(fallback = "Incoming Messages"))]
|
||||
IncomingMessages = 103,
|
||||
|
||||
@@ -317,6 +323,13 @@ pub enum StockMessage {
|
||||
|
||||
#[strum(props(fallback = "%1$s of %2$s used"))]
|
||||
PartOfTotallUsed = 116,
|
||||
|
||||
#[strum(props(fallback = "%1$s invited you to join this group.\n\n\
|
||||
Waiting for the device of %2$s to reply…"))]
|
||||
SecureJoinStarted = 117,
|
||||
|
||||
#[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
|
||||
SecureJoinReplies = 118,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
@@ -441,13 +454,6 @@ pub(crate) async fn status_line(context: &Context) -> String {
|
||||
translated(context, StockMessage::StatusLine).await
|
||||
}
|
||||
|
||||
/// Stock string: `Hello, I've just created the group "%1$s" for us.`.
|
||||
pub(crate) async fn new_group_draft(context: &Context, group_name: impl AsRef<str>) -> String {
|
||||
translated(context, StockMessage::NewGroupDraft)
|
||||
.await
|
||||
.replace1(group_name)
|
||||
}
|
||||
|
||||
/// Stock string: `Group name changed from "%1$s" to "%2$s".`.
|
||||
pub(crate) async fn msg_grp_name(
|
||||
context: &Context,
|
||||
@@ -582,6 +588,32 @@ pub(crate) async fn e2e_preferred(context: &Context) -> String {
|
||||
translated(context, StockMessage::E2ePreferred).await
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s invited you to join this group. Waiting for the device of %2$s to reply…`.
|
||||
pub(crate) async fn secure_join_started(context: &Context, inviter_contact_id: u32) -> String {
|
||||
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
|
||||
translated(context, StockMessage::SecureJoinStarted)
|
||||
.await
|
||||
.replace1(contact.get_name_n_addr())
|
||||
.replace2(contact.get_display_name())
|
||||
} else {
|
||||
format!(
|
||||
"secure_join_started: unknown contact {}",
|
||||
inviter_contact_id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s replied, waiting for being added to the group…`.
|
||||
pub(crate) async fn secure_join_replies(context: &Context, contact_id: u32) -> String {
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
||||
translated(context, StockMessage::SecureJoinReplies)
|
||||
.await
|
||||
.replace1(contact.get_display_name())
|
||||
} else {
|
||||
format!("secure_join_replies: unknown contact {}", contact_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s verified.`.
|
||||
pub(crate) async fn contact_verified(context: &Context, contact_addr: impl AsRef<str>) -> String {
|
||||
translated(context, StockMessage::ContactVerified)
|
||||
@@ -624,6 +656,16 @@ pub(crate) async fn ac_setup_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::AcSetupMsgBody).await
|
||||
}
|
||||
|
||||
/// Stock string: `Multi Device Synchronization`.
|
||||
pub(crate) async fn sync_msg_subject(context: &Context) -> String {
|
||||
translated(context, StockMessage::SyncMsgSubject).await
|
||||
}
|
||||
|
||||
/// Stock string: `This message is used to synchronize data betweeen your devices.`.
|
||||
pub(crate) async fn sync_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::SyncMsgBody).await
|
||||
}
|
||||
|
||||
/// Stock string: `Cannot login as \"%1$s\". Please check...`.
|
||||
pub(crate) async fn cannot_login(context: &Context, user: impl AsRef<str>) -> String {
|
||||
translated(context, StockMessage::CannotLogin)
|
||||
|
||||
348
src/summary.rs
348
src/summary.rs
@@ -7,15 +7,11 @@ use crate::context::Context;
|
||||
use crate::dc_tools::dc_truncate;
|
||||
use crate::message::{Message, MessageState};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::param::Param;
|
||||
use crate::stock_str;
|
||||
use itertools::Itertools;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
|
||||
// In practice, the user additionally cuts the string themselves
|
||||
// pixel-accurate.
|
||||
const SUMMARY_CHARACTERS: usize = 160;
|
||||
|
||||
/// Prefix displayed before message and separated by ":" in the chatlist.
|
||||
#[derive(Debug)]
|
||||
pub enum SummaryPrefix {
|
||||
@@ -85,15 +81,7 @@ impl Summary {
|
||||
}
|
||||
};
|
||||
|
||||
let mut text = get_summarytext_by_raw(
|
||||
msg.viewtype,
|
||||
msg.text.as_ref(),
|
||||
msg.is_forwarded(),
|
||||
&msg.param,
|
||||
SUMMARY_CHARACTERS,
|
||||
context,
|
||||
)
|
||||
.await;
|
||||
let mut text = msg.get_summary_text(context).await;
|
||||
|
||||
if text.is_empty() && msg.quoted_text().is_some() {
|
||||
text = stock_str::reply_noun(context).await
|
||||
@@ -106,88 +94,87 @@ impl Summary {
|
||||
state: msg.state,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`Summary::text`] attribute truncated to an approximate length.
|
||||
pub fn truncated_text(&self, approx_chars: usize) -> Cow<str> {
|
||||
dc_truncate(&self.text, approx_chars)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a summary text.
|
||||
pub async fn get_summarytext_by_raw(
|
||||
viewtype: Viewtype,
|
||||
text: Option<impl AsRef<str>>,
|
||||
was_forwarded: bool,
|
||||
param: &Params,
|
||||
approx_characters: usize,
|
||||
context: &Context,
|
||||
) -> String {
|
||||
let mut append_text = true;
|
||||
let prefix = match viewtype {
|
||||
Viewtype::Image => stock_str::image(context).await,
|
||||
Viewtype::Gif => stock_str::gif(context).await,
|
||||
Viewtype::Sticker => stock_str::sticker(context).await,
|
||||
Viewtype::Video => stock_str::video(context).await,
|
||||
Viewtype::Voice => stock_str::voice_message(context).await,
|
||||
Viewtype::Audio | Viewtype::File => {
|
||||
if param.get_cmd() == SystemMessage::AutocryptSetupMessage {
|
||||
append_text = false;
|
||||
stock_str::ac_setup_msg_subject(context).await
|
||||
} else {
|
||||
let file_name: String = param
|
||||
.get_path(Param::File, context)
|
||||
.unwrap_or(None)
|
||||
.and_then(|path| {
|
||||
path.file_name()
|
||||
.map(|fname| fname.to_string_lossy().into_owned())
|
||||
})
|
||||
.unwrap_or_else(|| String::from("ErrFileName"));
|
||||
let label = if viewtype == Viewtype::Audio {
|
||||
stock_str::audio(context).await
|
||||
impl Message {
|
||||
/// Returns a summary text.
|
||||
async fn get_summary_text(&self, context: &Context) -> String {
|
||||
let mut append_text = true;
|
||||
let prefix = match self.viewtype {
|
||||
Viewtype::Image => stock_str::image(context).await,
|
||||
Viewtype::Gif => stock_str::gif(context).await,
|
||||
Viewtype::Sticker => stock_str::sticker(context).await,
|
||||
Viewtype::Video => stock_str::video(context).await,
|
||||
Viewtype::Voice => stock_str::voice_message(context).await,
|
||||
Viewtype::Audio | Viewtype::File => {
|
||||
if self.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
|
||||
append_text = false;
|
||||
stock_str::ac_setup_msg_subject(context).await
|
||||
} else {
|
||||
stock_str::file(context).await
|
||||
};
|
||||
format!("{} – {}", label, file_name)
|
||||
let file_name: String = self
|
||||
.param
|
||||
.get_path(Param::File, context)
|
||||
.unwrap_or(None)
|
||||
.and_then(|path| {
|
||||
path.file_name()
|
||||
.map(|fname| fname.to_string_lossy().into_owned())
|
||||
})
|
||||
.unwrap_or_else(|| String::from("ErrFileName"));
|
||||
let label = if self.viewtype == Viewtype::Audio {
|
||||
stock_str::audio(context).await
|
||||
} else {
|
||||
stock_str::file(context).await
|
||||
};
|
||||
format!("{} – {}", label, file_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
Viewtype::VideochatInvitation => {
|
||||
append_text = false;
|
||||
stock_str::videochat_invitation(context).await
|
||||
}
|
||||
_ => {
|
||||
if param.get_cmd() != SystemMessage::LocationOnly {
|
||||
"".to_string()
|
||||
} else {
|
||||
Viewtype::VideochatInvitation => {
|
||||
append_text = false;
|
||||
stock_str::location(context).await
|
||||
stock_str::videochat_invitation(context).await
|
||||
}
|
||||
_ => {
|
||||
if self.param.get_cmd() != SystemMessage::LocationOnly {
|
||||
"".to_string()
|
||||
} else {
|
||||
append_text = false;
|
||||
stock_str::location(context).await
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !append_text {
|
||||
return prefix;
|
||||
}
|
||||
};
|
||||
|
||||
if !append_text {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
let summary_content = if let Some(text) = text {
|
||||
if text.as_ref().is_empty() {
|
||||
prefix
|
||||
} else if prefix.is_empty() {
|
||||
dc_truncate(text.as_ref(), approx_characters).to_string()
|
||||
let summary_content = if let Some(text) = &self.text {
|
||||
if text.is_empty() {
|
||||
prefix
|
||||
} else if prefix.is_empty() {
|
||||
text.to_string()
|
||||
} else {
|
||||
format!("{} – {}", prefix, text)
|
||||
}
|
||||
} else {
|
||||
let tmp = format!("{} – {}", prefix, text.as_ref());
|
||||
dc_truncate(&tmp, approx_characters).to_string()
|
||||
}
|
||||
} else {
|
||||
prefix
|
||||
};
|
||||
prefix
|
||||
};
|
||||
|
||||
let summary = if was_forwarded {
|
||||
let tmp = format!(
|
||||
"{}: {}",
|
||||
stock_str::forwarded(context).await,
|
||||
let summary = if self.is_forwarded() {
|
||||
format!(
|
||||
"{}: {}",
|
||||
stock_str::forwarded(context).await,
|
||||
summary_content
|
||||
)
|
||||
} else {
|
||||
summary_content
|
||||
);
|
||||
dc_truncate(&tmp, approx_characters).to_string()
|
||||
} else {
|
||||
summary_content
|
||||
};
|
||||
};
|
||||
|
||||
summary.split_whitespace().join(" ")
|
||||
summary.split_whitespace().collect::<Vec<&str>>().join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -196,7 +183,7 @@ mod tests {
|
||||
use crate::test_utils as test;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_summarytext_by_raw() {
|
||||
async fn test_get_summary_text() {
|
||||
let d = test::TestContext::new().await;
|
||||
let ctx = &d.ctx;
|
||||
|
||||
@@ -204,190 +191,125 @@ mod tests {
|
||||
let empty_text = Some("".to_string());
|
||||
let no_text: Option<String> = None;
|
||||
|
||||
let mut some_file = Params::new();
|
||||
some_file.set(Param::File, "foo.bar");
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(some_text.clone());
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Text,
|
||||
some_text.as_ref(),
|
||||
false,
|
||||
&Params::new(),
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"bla bla" // for simple text, the type is not added to the summary
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_text(no_text.clone());
|
||||
msg.set_file("foo.bar", None);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Image,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"Image" // file names are not added for images
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Video);
|
||||
msg.set_text(no_text.clone());
|
||||
msg.set_file("foo.bar", None);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Video,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"Video" // file names are not added for videos
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Gif);
|
||||
msg.set_text(no_text.clone());
|
||||
msg.set_file("foo.bar", None);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(Viewtype::Gif, no_text.as_ref(), false, &some_file, 50, ctx,)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"GIF" // file names are not added for GIFs
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_text(no_text.clone());
|
||||
msg.set_file("foo.bar", None);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Sticker,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx,
|
||||
)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"Sticker" // file names are not added for stickers
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Voice);
|
||||
msg.set_text(empty_text.clone());
|
||||
msg.set_file("foo.bar", None);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Voice,
|
||||
empty_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx,
|
||||
)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"Voice message" // file names are not added for voice messages, empty text is skipped
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Voice);
|
||||
msg.set_text(no_text.clone());
|
||||
msg.set_file("foo.bar", None);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Voice,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"Voice message" // file names are not added for voice messages
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Voice);
|
||||
msg.set_text(some_text.clone());
|
||||
msg.set_file("foo.bar", None);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Voice,
|
||||
some_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"Voice message \u{2013} bla bla" // `\u{2013}` explicitly checks for "EN DASH"
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Audio);
|
||||
msg.set_text(no_text.clone());
|
||||
msg.set_file("foo.bar", None);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Audio,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"Audio \u{2013} foo.bar" // file name is added for audio
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Audio);
|
||||
msg.set_text(empty_text.clone());
|
||||
msg.set_file("foo.bar", None);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Audio,
|
||||
empty_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx,
|
||||
)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"Audio \u{2013} foo.bar" // file name is added for audio, empty text is not added
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Audio);
|
||||
msg.set_text(some_text.clone());
|
||||
msg.set_file("foo.bar", None);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Audio,
|
||||
some_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"Audio \u{2013} foo.bar \u{2013} bla bla" // file name and text added for audio
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_text(some_text.clone());
|
||||
msg.set_file("foo.bar", None);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::File,
|
||||
some_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"File \u{2013} foo.bar \u{2013} bla bla" // file name is added for files
|
||||
);
|
||||
|
||||
// Forwarded
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(some_text.clone());
|
||||
msg.param.set_int(Param::Forwarded, 1);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Text,
|
||||
some_text.as_ref(),
|
||||
true,
|
||||
&Params::new(),
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"Forwarded: bla bla" // for simple text, the type is not added to the summary
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_text(some_text.clone());
|
||||
msg.set_file("foo.bar", None);
|
||||
msg.param.set_int(Param::Forwarded, 1);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::File,
|
||||
some_text.as_ref(),
|
||||
true,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"Forwarded: File \u{2013} foo.bar \u{2013} bla bla"
|
||||
);
|
||||
|
||||
let mut asm_file = Params::new();
|
||||
asm_file.set(Param::File, "foo.bar");
|
||||
asm_file.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_text(no_text.clone());
|
||||
msg.param.set(Param::File, "foo.bar");
|
||||
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(Viewtype::File, no_text.as_ref(), false, &asm_file, 50, ctx)
|
||||
.await,
|
||||
msg.get_summary_text(ctx).await,
|
||||
"Autocrypt Setup Message" // file name is not added for autocrypt setup messages
|
||||
);
|
||||
}
|
||||
|
||||
508
src/sync.rs
Normal file
508
src/sync.rs
Normal file
@@ -0,0 +1,508 @@
|
||||
//! # Synchronize items between devices.
|
||||
|
||||
use crate::chat::{Chat, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::sync::SyncData::{AddQrToken, DeleteQrToken};
|
||||
use crate::token::Namespace;
|
||||
use crate::{chat, stock_str, token};
|
||||
use anyhow::Result;
|
||||
use lettre_email::mime::{self};
|
||||
use lettre_email::PartBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct QrTokenData {
|
||||
pub(crate) invitenumber: String,
|
||||
pub(crate) auth: String,
|
||||
pub(crate) grpid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) enum SyncData {
|
||||
AddQrToken(QrTokenData),
|
||||
DeleteQrToken(QrTokenData),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct SyncItem {
|
||||
timestamp: i64,
|
||||
data: SyncData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct SyncItems {
|
||||
items: Vec<SyncItem>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Checks if sync messages shall be sent.
|
||||
/// Receiving sync messages is currently always enabled;
|
||||
/// the messages are force-encrypted anyway.
|
||||
async fn is_sync_sending_enabled(&self) -> Result<bool> {
|
||||
self.get_config_bool(Config::SendSyncMsgs).await
|
||||
}
|
||||
|
||||
/// Adds an item to the list of items that should be synchronized to other devices.
|
||||
pub(crate) async fn add_sync_item(&self, data: SyncData) -> Result<()> {
|
||||
self.add_sync_item_with_timestamp(data, time()).await
|
||||
}
|
||||
|
||||
/// Adds item and timestamp to the list of items that should be synchronized to other devices.
|
||||
/// If device synchronization is disabled, the function does nothing.
|
||||
async fn add_sync_item_with_timestamp(&self, data: SyncData, timestamp: i64) -> Result<()> {
|
||||
if !self.is_sync_sending_enabled().await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let item = SyncItem { timestamp, data };
|
||||
let item = serde_json::to_string(&item)?;
|
||||
self.sql
|
||||
.execute(
|
||||
"INSERT INTO multi_device_sync (item) VALUES(?);",
|
||||
paramsv![item],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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<ChatId>) -> Result<()> {
|
||||
if !self.is_sync_sending_enabled().await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let (Some(invitenumber), Some(auth)) = (
|
||||
token::lookup(self, Namespace::InviteNumber, chat_id).await?,
|
||||
token::lookup(self, Namespace::Auth, chat_id).await?,
|
||||
) {
|
||||
let grpid = if let Some(chat_id) = chat_id {
|
||||
let chat = Chat::load_from_db(self, chat_id).await?;
|
||||
if !chat.is_promoted() {
|
||||
info!(
|
||||
self,
|
||||
"group '{}' not yet promoted, do not sync tokens yet.", chat.grpid
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
Some(chat.grpid)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.add_sync_item(SyncData::AddQrToken(QrTokenData {
|
||||
invitenumber,
|
||||
auth,
|
||||
grpid,
|
||||
}))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Add deleted qr-code token to the list of items to be synced
|
||||
// so that the token also gets deleted on the other devices.
|
||||
pub(crate) async fn sync_qr_code_token_deletion(
|
||||
&self,
|
||||
invitenumber: String,
|
||||
auth: String,
|
||||
) -> Result<()> {
|
||||
self.add_sync_item(SyncData::DeleteQrToken(QrTokenData {
|
||||
invitenumber,
|
||||
auth,
|
||||
grpid: None,
|
||||
}))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Sends out a self-sent message with items to be synchronized, if any.
|
||||
pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
|
||||
if let Some((json, ids)) = self.build_sync_json().await? {
|
||||
let chat_id =
|
||||
ChatId::create_for_contact_with_blocked(self, DC_CONTACT_ID_SELF, Blocked::Yes)
|
||||
.await?;
|
||||
let mut msg = Message {
|
||||
chat_id,
|
||||
viewtype: Viewtype::Text,
|
||||
text: Some(stock_str::sync_msg_body(self).await),
|
||||
hidden: true,
|
||||
subject: stock_str::sync_msg_subject(self).await,
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(SystemMessage::MultiDeviceSync);
|
||||
msg.param.set(Param::Arg, json);
|
||||
msg.param.set(Param::Arg2, ids);
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
Ok(Some(chat::send_msg(self, chat_id, &mut msg).await?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies all sync items to a JSON string and clears the sync-table.
|
||||
/// Returns the JSON string and a comma-separated string of the IDs used.
|
||||
pub(crate) async fn build_sync_json(&self) -> Result<Option<(String, String)>> {
|
||||
let (ids, serialized) = self
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, item FROM multi_device_sync ORDER BY id;",
|
||||
paramsv![],
|
||||
|row| Ok((row.get::<_, u32>(0)?, row.get::<_, String>(1)?)),
|
||||
|rows| {
|
||||
let mut ids = vec![];
|
||||
let mut serialized = String::default();
|
||||
for row in rows {
|
||||
let (id, item) = row?;
|
||||
ids.push(id);
|
||||
if !serialized.is_empty() {
|
||||
serialized.push_str(",\n");
|
||||
}
|
||||
serialized.push_str(&item);
|
||||
}
|
||||
Ok((ids, serialized))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
if ids.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some((
|
||||
format!("{{\"items\":[\n{}\n]}}", serialized),
|
||||
ids.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(","),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn build_sync_part(&self, json: String) -> PartBuilder {
|
||||
PartBuilder::new()
|
||||
.content_type(&"application/json".parse::<mime::Mime>().unwrap())
|
||||
.header((
|
||||
"Content-Disposition",
|
||||
"attachment; filename=\"multi-device-sync.json\"",
|
||||
))
|
||||
.body(json)
|
||||
}
|
||||
|
||||
/// Deletes IDs as returned by `build_sync_json()`.
|
||||
pub(crate) async fn delete_sync_ids(&self, ids: String) -> Result<()> {
|
||||
self.sql
|
||||
.execute(
|
||||
format!("DELETE FROM multi_device_sync WHERE id IN ({});", ids),
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Takes a JSON string created by `build_sync_json()`
|
||||
/// and construct `SyncItems` from it.
|
||||
pub(crate) async fn parse_sync_items(&self, serialized: String) -> Result<SyncItems> {
|
||||
let sync_items: SyncItems = serde_json::from_str(&serialized)?;
|
||||
Ok(sync_items)
|
||||
}
|
||||
|
||||
/// Execute sync items.
|
||||
///
|
||||
/// CAVE: When changing the code to handle other sync items,
|
||||
/// take care that does not result in calls to `add_sync_item()`
|
||||
/// 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<()> {
|
||||
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?;
|
||||
}
|
||||
DeleteQrToken(token) => {
|
||||
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
|
||||
token::delete(self, Namespace::Auth, &token.auth).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::Chat;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::token::Namespace;
|
||||
use anyhow::bail;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_is_sync_sending_enabled() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert!(!t.is_sync_sending_enabled().await?);
|
||||
t.set_config_bool(Config::SendSyncMsgs, true).await?;
|
||||
assert!(t.is_sync_sending_enabled().await?);
|
||||
t.set_config_bool(Config::SendSyncMsgs, false).await?;
|
||||
assert!(!t.is_sync_sending_enabled().await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_build_sync_json() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config_bool(Config::SendSyncMsgs, true).await?;
|
||||
|
||||
assert!(t.build_sync_json().await?.is_none());
|
||||
|
||||
t.add_sync_item_with_timestamp(
|
||||
SyncData::AddQrToken(QrTokenData {
|
||||
invitenumber: "testinvite".to_string(),
|
||||
auth: "testauth".to_string(),
|
||||
grpid: Some("group123".to_string()),
|
||||
}),
|
||||
1631781316,
|
||||
)
|
||||
.await?;
|
||||
t.add_sync_item_with_timestamp(
|
||||
SyncData::DeleteQrToken(QrTokenData {
|
||||
invitenumber: "123!?\":.;{}".to_string(),
|
||||
auth: "456".to_string(),
|
||||
grpid: None,
|
||||
}),
|
||||
1631781317,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (serialized, ids) = t.build_sync_json().await?.unwrap();
|
||||
assert_eq!(
|
||||
serialized,
|
||||
r#"{"items":[
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"testinvite","auth":"testauth","grpid":"group123"}}},
|
||||
{"timestamp":1631781317,"data":{"DeleteQrToken":{"invitenumber":"123!?\":.;{}","auth":"456","grpid":null}}}
|
||||
]}"#
|
||||
);
|
||||
|
||||
assert!(t.build_sync_json().await?.is_some());
|
||||
t.delete_sync_ids(ids).await?;
|
||||
assert!(t.build_sync_json().await?.is_none());
|
||||
|
||||
let sync_items = t.parse_sync_items(serialized).await?;
|
||||
assert_eq!(sync_items.items.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_build_sync_json_sync_msgs_off() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config_bool(Config::SendSyncMsgs, false).await?;
|
||||
t.add_sync_item(SyncData::AddQrToken(QrTokenData {
|
||||
invitenumber: "testinvite".to_string(),
|
||||
auth: "testauth".to_string(),
|
||||
grpid: Some("group123".to_string()),
|
||||
}))
|
||||
.await?;
|
||||
assert!(t.build_sync_json().await?.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_sync_items() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
assert!(t
|
||||
.parse_sync_items(r#"{bad json}"#.to_string())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
assert!(t
|
||||
.parse_sync_items(r#"{"badname":[]}"#.to_string())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"BadItem":{"invitenumber":"in","auth":"a","grpid":null}}}]}"#
|
||||
.to_string(),
|
||||
)
|
||||
.await.is_err());
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":123}}}]}"#.to_string(),
|
||||
)
|
||||
.await
|
||||
.is_err()); // `123` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":true}}}]}"#.to_string(),
|
||||
)
|
||||
.await
|
||||
.is_err()); // `true` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":[]}}}]}"#.to_string(),
|
||||
)
|
||||
.await
|
||||
.is_err()); // `[]` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":{}}}}]}"#.to_string(),
|
||||
)
|
||||
.await
|
||||
.is_err()); // `{}` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","grpid":null}}}]}"#.to_string(),
|
||||
)
|
||||
.await
|
||||
.is_err()); // missing field
|
||||
|
||||
// empty item list is okay
|
||||
assert_eq!(
|
||||
t.parse_sync_items(r#"{"items":[]}"#.to_string())
|
||||
.await?
|
||||
.items
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
|
||||
// to allow forward compatibility, additional fields should not break parsing
|
||||
let sync_items = t
|
||||
.parse_sync_items(
|
||||
r#"{"items":[
|
||||
{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"yip","grpid":null}}},
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip","additional":123,"grpid":null}}}
|
||||
]}"#
|
||||
.to_string(),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(sync_items.items.len(), 2);
|
||||
|
||||
let sync_items = t
|
||||
.parse_sync_items(
|
||||
r#"{"items":[
|
||||
{"timestamp":1631781318,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip","grpid":null}}}
|
||||
],"additional":"field"}"#
|
||||
.to_string(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(sync_items.items.len(), 1);
|
||||
if let AddQrToken(token) = &sync_items.items.get(0).unwrap().data {
|
||||
assert_eq!(token.invitenumber, "in");
|
||||
assert_eq!(token.auth, "yip");
|
||||
assert_eq!(token.grpid, None);
|
||||
} else {
|
||||
bail!("bad item");
|
||||
}
|
||||
|
||||
// to allow backward compatibility, missing `Option<>` should not break parsing
|
||||
let sync_items = t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781319,"data":{"AddQrToken":{"invitenumber":"in","auth":"a"}}}]}"#.to_string(),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(sync_items.items.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_execute_sync_items() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
assert!(!token::exists(&t, Namespace::Auth, "yip-auth").await);
|
||||
|
||||
let sync_items = t
|
||||
.parse_sync_items(
|
||||
r#"{"items":[
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"yip-in","auth":"a"}}},
|
||||
{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"delete unexistant, shall continue"}}},
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip-auth"}}},
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"foo","grpid":"non-existant"}}},
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"directly deleted"}}},
|
||||
{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"directly deleted"}}}
|
||||
]}"#
|
||||
.to_string(),
|
||||
)
|
||||
.await?;
|
||||
t.execute_sync_items(&sync_items).await?;
|
||||
|
||||
assert!(token::exists(&t, Namespace::InviteNumber, "yip-in").await);
|
||||
assert!(token::exists(&t, Namespace::Auth, "yip-auth").await);
|
||||
assert!(!token::exists(&t, Namespace::Auth, "non-existant").await);
|
||||
assert!(!token::exists(&t, Namespace::Auth, "directly deleted").await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_send_sync_msg() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
alice.set_config_bool(Config::SendSyncMsgs, true).await?;
|
||||
alice
|
||||
.add_sync_item(SyncData::AddQrToken(QrTokenData {
|
||||
invitenumber: "in".to_string(),
|
||||
auth: "testtoken".to_string(),
|
||||
grpid: None,
|
||||
}))
|
||||
.await?;
|
||||
let msg_id = alice.send_sync_msg().await?.unwrap();
|
||||
let msg = Message::load_from_db(&alice, msg_id).await?;
|
||||
let chat = Chat::load_from_db(&alice, msg.chat_id).await?;
|
||||
assert!(chat.is_self_talk());
|
||||
|
||||
// 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 = ChatId::create_for_contact(&alice, DC_CONTACT_ID_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);
|
||||
let msgs = chat::get_chat_msgs(&alice, chat_id, 0, None).await?;
|
||||
assert_eq!(msgs.len(), 0);
|
||||
|
||||
// let alice's other device receive and execute the sync message,
|
||||
// also here, self-talk should stay hidden
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
alice2.recv_msg(&sent_msg).await;
|
||||
assert!(token::exists(&alice2, token::Namespace::Auth, "testtoken").await);
|
||||
assert_eq!(Chatlist::try_load(&alice2, 0, None, None).await?.len(), 0);
|
||||
|
||||
// the same sync message sent to bob must not be executed
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
assert!(!token::exists(&bob, token::Namespace::Auth, "testtoken").await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,9 @@ use crate::message::{update_msg_state, Message, MessageState, MsgId};
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::param::{Param, Params};
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
|
||||
type EventSink =
|
||||
dyn Fn(Event) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> + Send + Sync + 'static;
|
||||
|
||||
@@ -108,12 +111,15 @@ impl TestContext {
|
||||
let (evtracker_sender, evtracker_receiver) = channel::unbounded();
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
// Make sure that the test fails if there is a panic on this thread here:
|
||||
let current_id = task::current().id();
|
||||
// Make sure that the test fails if there is a panic on this thread here
|
||||
// (but not if there is a panic on another thread)
|
||||
let looptask_id = task::current().id();
|
||||
let orig_hook = panic::take_hook();
|
||||
panic::set_hook(Box::new(move |panic_info| {
|
||||
if task::current().id() == current_id {
|
||||
poison_sender.try_send(panic_info.to_string()).ok();
|
||||
if let Some(panicked_task) = task::try_current() {
|
||||
if panicked_task.id() == looptask_id {
|
||||
poison_sender.try_send(panic_info.to_string()).ok();
|
||||
}
|
||||
}
|
||||
orig_hook(panic_info);
|
||||
}));
|
||||
@@ -587,6 +593,21 @@ impl EvTracker {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_matching<F: Fn(EventType) -> bool>(&self, event_matcher: F) -> EventType {
|
||||
const TIMEOUT: Duration = Duration::from_secs(20);
|
||||
|
||||
loop {
|
||||
let event = async_std::future::timeout(TIMEOUT, self.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
if event_matcher(event.clone()) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for EvTracker {
|
||||
|
||||
Reference in New Issue
Block a user