mirror of
https://github.com/chatmail/core.git
synced 2026-05-18 06:16:29 +03:00
Compare commits
1 Commits
1.63.0
...
no-protect
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3253d427ec |
37
CHANGELOG.md
37
CHANGELOG.md
@@ -1,42 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
51
Cargo.lock
generated
51
Cargo.lock
generated
@@ -10,9 +10,9 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.17.0"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
|
||||
checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
@@ -114,9 +114,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.45"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7"
|
||||
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
@@ -423,9 +423,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.63"
|
||||
version = "0.3.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6"
|
||||
checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
@@ -1072,7 +1072,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.63.0"
|
||||
version = "1.61.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1131,7 +1131,7 @@ dependencies = [
|
||||
"sha-1",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"stop-token 0.6.1",
|
||||
"stop-token 0.5.1",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"surf",
|
||||
@@ -1152,7 +1152,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.63.0"
|
||||
version = "1.61.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
@@ -1734,9 +1734,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.26.0"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81a03ce013ffccead76c11a15751231f777d9295b845cc1266ed4d34fcbd7977"
|
||||
checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7"
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
@@ -2104,9 +2104,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.106"
|
||||
version = "0.2.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673"
|
||||
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -2417,9 +2417,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.27.0"
|
||||
version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c821014c18301591b89b843809ef953af9e3df0496c232d5c0611b0a52aac363"
|
||||
checksum = "39f37e50073ccad23b6d09bcb5b263f4e76d3bb6038e4a3c08e52162ffa8abc2"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -2801,9 +2801,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.32"
|
||||
version = "1.0.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
|
||||
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
@@ -3324,9 +3324,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.69"
|
||||
version = "1.0.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e466864e431129c7e0d3476b92f20458e5879919a0596c6472738d9fa2d342f8"
|
||||
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -3566,9 +3566,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "stop-token"
|
||||
version = "0.6.1"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3829f0fc642e19ef8a1072ba8e33139a385f609ba56761df501c5272a9704918"
|
||||
checksum = "561eec821556b41afc80e8943d59ff72e5dc9876c364f26331293f2eb6c8e461"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"cfg-if 1.0.0",
|
||||
@@ -3614,11 +3614,10 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||
|
||||
[[package]]
|
||||
name = "surf"
|
||||
version = "2.3.2"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "718b1ae6b50351982dedff021db0def601677f2120938b070eadb10ba4038dd7"
|
||||
checksum = "73f856d60bdb4679fc9ec516c34093484e963431b5016a8429f85a8e74b5ccaa"
|
||||
dependencies = [
|
||||
"async-native-tls",
|
||||
"async-std",
|
||||
"async-trait",
|
||||
"cfg-if 1.0.0",
|
||||
@@ -3636,9 +3635,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.81"
|
||||
version = "1.0.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
|
||||
checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.63.0"
|
||||
version = "1.61.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -65,7 +65,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.9"
|
||||
sha2 = "0.9"
|
||||
smallvec = "1"
|
||||
stop-token = "0.6"
|
||||
stop-token = "0.5"
|
||||
strum = "0.22"
|
||||
strum_macros = "0.22"
|
||||
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.63.0"
|
||||
version = "1.61.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -2249,12 +2249,21 @@ 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.
|
||||
*
|
||||
* 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.
|
||||
* 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().
|
||||
*
|
||||
* Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes.
|
||||
* - 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.
|
||||
*
|
||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
@@ -2264,8 +2273,10 @@ 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.
|
||||
* On errors, 0 is returned, however, most errors will happen during handshake later on.
|
||||
* If the out-of-band verification failed or was aborted, 0 is returned.
|
||||
* 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);
|
||||
|
||||
@@ -2414,22 +2425,6 @@ 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
|
||||
@@ -5373,14 +5368,6 @@ 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
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@@ -5964,21 +5951,6 @@ 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.
|
||||
@@ -6053,26 +6025,6 @@ 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
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -438,7 +438,6 @@ 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, .. }
|
||||
@@ -489,7 +488,6 @@ 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, .. }
|
||||
@@ -539,7 +537,6 @@ 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 {
|
||||
@@ -2206,16 +2203,6 @@ 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;
|
||||
|
||||
@@ -386,7 +386,6 @@ 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\
|
||||
@@ -425,7 +424,6 @@ 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\
|
||||
@@ -897,10 +895,6 @@ 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?;
|
||||
|
||||
@@ -167,7 +167,7 @@ const DB_COMMANDS: [&str; 10] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 35] = [
|
||||
const CHAT_COMMANDS: [&str; 34] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"chat",
|
||||
@@ -188,7 +188,6 @@ const CHAT_COMMANDS: [&str; 35] = [
|
||||
"sendimage",
|
||||
"sendfile",
|
||||
"sendhtml",
|
||||
"sendsyncmsg",
|
||||
"videochat",
|
||||
"draft",
|
||||
"listmedia",
|
||||
@@ -224,11 +223,10 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"unblock",
|
||||
"listblocked",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 11] = [
|
||||
const MISC_COMMANDS: [&str; 10] = [
|
||||
"getqr",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"joinqr",
|
||||
"event",
|
||||
"fileinfo",
|
||||
"clear",
|
||||
|
||||
@@ -7,5 +7,3 @@
|
||||
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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0"]
|
||||
|
||||
[tool.setuptools_scm]
|
||||
root = ".."
|
||||
tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
|
||||
git_describe_command = "git describe --dirty --tags --long --match py-*.*"
|
||||
@@ -8,6 +8,13 @@ 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',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import mimetypes
|
||||
import calendar
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
import os
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
||||
from .capi import lib, ffi
|
||||
@@ -512,9 +512,8 @@ 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.fromtimestamp(
|
||||
lib.dc_array_get_timestamp(dc_array, i),
|
||||
timezone.utc
|
||||
timestamp=datetime.utcfromtimestamp(
|
||||
lib.dc_array_get_timestamp(dc_array, i)
|
||||
),
|
||||
marker=from_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from .capi import lib
|
||||
from .capi import ffi
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def as_dc_charpointer(obj):
|
||||
@@ -44,4 +44,4 @@ class DCLot:
|
||||
ts = lib.dc_lot_get_timestamp(self._dc_lot)
|
||||
if ts == 0:
|
||||
return None
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
|
||||
@@ -6,7 +6,7 @@ from . import props
|
||||
from .cutil import from_dc_charpointer, as_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
from . import const
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Message(object):
|
||||
@@ -170,7 +170,7 @@ class Message(object):
|
||||
:returns: naive datetime.datetime() object.
|
||||
"""
|
||||
ts = lib.dc_msg_get_timestamp(self._dc_msg)
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
|
||||
@props.with_doc
|
||||
def time_received(self):
|
||||
@@ -180,7 +180,7 @@ class Message(object):
|
||||
"""
|
||||
ts = lib.dc_msg_get_received_timestamp(self._dc_msg)
|
||||
if ts:
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
|
||||
@props.with_doc
|
||||
def ephemeral_timer(self):
|
||||
@@ -200,7 +200,7 @@ class Message(object):
|
||||
"""
|
||||
ts = lib.dc_msg_get_ephemeral_timestamp(self._dc_msg)
|
||||
if ts:
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
|
||||
@property
|
||||
def quoted_text(self):
|
||||
|
||||
@@ -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, timezone
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
@pytest.mark.parametrize("msgtext,res", [
|
||||
@@ -447,7 +447,7 @@ class TestOfflineChat:
|
||||
contact1.create_chat().send_text("hello")
|
||||
|
||||
def test_chat_message_distinctions(self, ac1, chat1):
|
||||
past1s = datetime.now(timezone.utc) - timedelta(seconds=1)
|
||||
past1s = datetime.utcnow() - timedelta(seconds=1)
|
||||
msg = chat1.send_text("msg1")
|
||||
ts = msg.time_sent
|
||||
assert msg.time_received is None
|
||||
@@ -1221,40 +1221,6 @@ 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
|
||||
@@ -2197,7 +2163,7 @@ class TestOnlineAccount:
|
||||
break # DC is done with reading messages
|
||||
|
||||
def test_send_receive_locations(self, acfactory, lp):
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.utcnow()
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
|
||||
@@ -45,8 +45,9 @@ commands =
|
||||
[testenv:doc]
|
||||
changedir=doc
|
||||
deps =
|
||||
sphinx
|
||||
breathe
|
||||
# Pin dependencies to the versions which actually work with Python 3.5.
|
||||
sphinx==3.4.3
|
||||
breathe==4.28.0
|
||||
commands =
|
||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
||||
|
||||
|
||||
@@ -621,10 +621,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::{
|
||||
message::Message,
|
||||
test_utils::{self, TestContext},
|
||||
};
|
||||
use crate::{message::Message, test_utils::TestContext};
|
||||
use image::Pixel;
|
||||
|
||||
#[async_std::test]
|
||||
@@ -873,10 +870,11 @@ 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(test_utils::AVATAR_900x900_BYTES)
|
||||
.write_all(avatar_bytes)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
176
src/chat.rs
176
src/chat.rs
@@ -171,20 +171,9 @@ 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 create_blocked == Blocked::Not && chat.blocked != Blocked::Not {
|
||||
if chat.blocked != Blocked::Not {
|
||||
chat.id.unblock(context).await?;
|
||||
}
|
||||
chat.id
|
||||
@@ -193,10 +182,7 @@ impl ChatId {
|
||||
if Contact::real_exists_by_id(context, contact_id).await?
|
||||
|| contact_id == DC_CONTACT_ID_SELF
|
||||
{
|
||||
let chat_id =
|
||||
ChatIdBlocked::get_for_contact(context, contact_id, create_blocked)
|
||||
.await
|
||||
.map(|chat| chat.id)?;
|
||||
let chat_id = ChatId::get_for_contact(context, contact_id).await?;
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat).await?;
|
||||
chat_id
|
||||
} else {
|
||||
@@ -266,7 +252,7 @@ impl ChatId {
|
||||
/// Updates chat blocked status.
|
||||
///
|
||||
/// Returns true if the value was modified.
|
||||
pub(crate) async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
|
||||
async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
|
||||
if self.is_special() {
|
||||
bail!("ignoring setting of Block-status for {}", self);
|
||||
}
|
||||
@@ -304,7 +290,7 @@ impl ChatId {
|
||||
self.delete(context).await?;
|
||||
}
|
||||
Chattype::Mailinglist => {
|
||||
if self.set_blocked(context, Blocked::Yes).await? {
|
||||
if self.set_blocked(context, Blocked::Manually).await? {
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
}
|
||||
}
|
||||
@@ -397,7 +383,7 @@ impl ChatId {
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
|
||||
// make sure, the receivers will get all keys
|
||||
self.reset_gossiped_timestamp(context).await?;
|
||||
reset_gossiped_timestamp(context, self).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -857,48 +843,6 @@ 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 {
|
||||
@@ -1099,6 +1043,10 @@ 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;
|
||||
|
||||
@@ -1131,7 +1079,7 @@ impl Chat {
|
||||
name: self.name.clone(),
|
||||
archived: self.visibility == ChatVisibility::Archived,
|
||||
param: self.param.to_string(),
|
||||
gossiped_timestamp: self.id.get_gossiped_timestamp(context).await?,
|
||||
gossiped_timestamp: self.get_gossiped_timestamp(context).await?,
|
||||
is_sending_locations: self.is_sending_locations,
|
||||
color: self.get_color(context).await?,
|
||||
profile_image: self
|
||||
@@ -1243,10 +1191,6 @@ 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
|
||||
@@ -2409,7 +2353,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();
|
||||
|
||||
chat_id.reset_gossiped_timestamp(context).await?;
|
||||
reset_gossiped_timestamp(context, chat_id).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?;
|
||||
@@ -2439,8 +2383,6 @@ 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)
|
||||
@@ -2491,6 +2433,45 @@ 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.
|
||||
@@ -3255,7 +3236,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_self_talk() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = TestContext::new().await;
|
||||
let chat = &t.get_self_chat().await;
|
||||
assert_eq!(DC_CONTACT_ID_SELF, 1);
|
||||
assert!(!chat.id.is_special());
|
||||
@@ -3265,23 +3246,6 @@ 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(())
|
||||
}
|
||||
|
||||
@@ -3890,7 +3854,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::Yes)
|
||||
let chat_id = ChatIdBlocked::get_for_contact(&ctx, contact_id, Blocked::Manually)
|
||||
.await
|
||||
.unwrap()
|
||||
.id;
|
||||
@@ -3899,7 +3863,7 @@ mod tests {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(chat_id, chat2.id);
|
||||
assert_eq!(chat2.blocked, Blocked::Yes);
|
||||
assert_eq!(chat2.blocked, Blocked::Manually);
|
||||
|
||||
// test nonexistent contact
|
||||
let found = ChatId::lookup_by_contact(&ctx, 1234).await.unwrap();
|
||||
@@ -4409,38 +4373,4 @@ 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::Yes, ChatVisibility::Archived],
|
||||
paramsv![Blocked::Manually, ChatVisibility::Archived],
|
||||
)
|
||||
.await?;
|
||||
Ok(count)
|
||||
|
||||
@@ -175,11 +175,6 @@ 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 {
|
||||
@@ -284,13 +279,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;
|
||||
|
||||
@@ -24,7 +24,7 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
|
||||
#[repr(i8)]
|
||||
pub enum Blocked {
|
||||
Not = 0,
|
||||
Yes = 1,
|
||||
Manually = 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::Yes, Blocked::from_i32(1).unwrap());
|
||||
assert_eq!(Blocked::Manually, Blocked::from_i32(1).unwrap());
|
||||
assert_eq!(Blocked::Request, Blocked::from_i32(2).unwrap());
|
||||
}
|
||||
|
||||
|
||||
@@ -711,7 +711,7 @@ impl Contact {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT name, grpid FROM chats WHERE type=? AND blocked=?;",
|
||||
paramsv![Chattype::Mailinglist, Blocked::Yes],
|
||||
paramsv![Chattype::Mailinglist, Blocked::Manually],
|
||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
@@ -1371,14 +1371,11 @@ fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use async_std::fs::File;
|
||||
use async_std::io::WriteExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::chat::send_text_msg;
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::{self, TestContext};
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
fn test_may_be_valid_addr() {
|
||||
@@ -1974,64 +1971,4 @@ 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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,11 +76,6 @@ 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)]
|
||||
@@ -152,7 +147,6 @@ 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 {
|
||||
@@ -309,7 +303,6 @@ 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
|
||||
@@ -399,7 +392,6 @@ 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);
|
||||
|
||||
@@ -142,6 +142,8 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
};
|
||||
|
||||
// the function returns the number of created messages in the database
|
||||
let mut hidden = false;
|
||||
|
||||
let mut needs_delete_job = false;
|
||||
|
||||
let mut created_db_entries = Vec::new();
|
||||
@@ -201,6 +203,7 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
sent_timestamp,
|
||||
rcvd_timestamp,
|
||||
from_id,
|
||||
&mut hidden,
|
||||
seen || replace_partial_download,
|
||||
is_partial_download,
|
||||
&mut needs_delete_job,
|
||||
@@ -212,46 +215,22 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
.await
|
||||
.map_err(|err| err.context("add_parts error"))?;
|
||||
|
||||
// Update gossiped timestamp for the chat if someone else or our other device sent
|
||||
// Autocrypt-Gossip for all recipients in the chat to avoid sending Autocrypt-Gossip ourselves
|
||||
// and waste traffic.
|
||||
if !chat_id.is_special()
|
||||
&& mime_parser
|
||||
.recipients
|
||||
.iter()
|
||||
.all(|recipient| mime_parser.gossiped_addr.contains(&recipient.addr))
|
||||
{
|
||||
info!(
|
||||
context,
|
||||
"Received message contains Autocrypt-Gossip for all members, updating timestamp."
|
||||
);
|
||||
if chat_id.get_gossiped_timestamp(context).await? < sent_timestamp {
|
||||
chat_id
|
||||
.set_gossiped_timestamp(context, sent_timestamp)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let insert_msg_id = if let Some((_chat_id, msg_id)) = created_db_entries.last() {
|
||||
*msg_id
|
||||
} else {
|
||||
MsgId::new_unset()
|
||||
};
|
||||
|
||||
save_locations(context, &mime_parser, chat_id, from_id, insert_msg_id).await?;
|
||||
|
||||
if let Some(ref sync_items) = mime_parser.sync_items {
|
||||
if from_id == DC_CONTACT_ID_SELF {
|
||||
if mime_parser.was_encrypted() {
|
||||
if let Err(err) = context.execute_sync_items(sync_items).await {
|
||||
warn!(context, "receive_imf cannot execute sync items: {}", err);
|
||||
}
|
||||
} else {
|
||||
warn!(context, "sync items are not encrypted.");
|
||||
}
|
||||
} else {
|
||||
warn!(context, "sync items not sent by self.");
|
||||
}
|
||||
if mime_parser.location_kml.is_some() || mime_parser.message_kml.is_some() {
|
||||
save_locations(
|
||||
context,
|
||||
&mime_parser,
|
||||
chat_id,
|
||||
from_id,
|
||||
insert_msg_id,
|
||||
hidden,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.user_avatar {
|
||||
@@ -416,7 +395,7 @@ pub async fn from_field_to_contact_id(
|
||||
#[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
|
||||
async fn add_parts(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
mut mime_parser: &mut MimeMessage,
|
||||
imf_raw: &[u8],
|
||||
incoming: bool,
|
||||
incoming_origin: Origin,
|
||||
@@ -427,6 +406,7 @@ async fn add_parts(
|
||||
sent_timestamp: i64,
|
||||
rcvd_timestamp: i64,
|
||||
from_id: u32,
|
||||
hidden: &mut bool,
|
||||
seen: bool,
|
||||
is_partial_download: Option<u32>,
|
||||
needs_delete_job: &mut bool,
|
||||
@@ -435,13 +415,14 @@ async fn add_parts(
|
||||
fetching_existing_messages: bool,
|
||||
prevent_rename: bool,
|
||||
) -> Result<ChatId> {
|
||||
let mut state: MessageState;
|
||||
let mut chat_id = None;
|
||||
let mut chat_id_blocked = Blocked::Not;
|
||||
let mut incoming_origin = incoming_origin;
|
||||
|
||||
let parent = get_parent_message(context, mime_parser).await?;
|
||||
|
||||
let is_dc_message = if mime_parser.has_chat_version() {
|
||||
let mut is_dc_message = if mime_parser.has_chat_version() {
|
||||
MessengerMessage::Yes
|
||||
} else if let Some(parent) = &parent {
|
||||
match parent.is_dc_message {
|
||||
@@ -453,12 +434,10 @@ async fn add_parts(
|
||||
};
|
||||
// incoming non-chat messages may be discarded
|
||||
|
||||
let location_kml_is = mime_parser.location_kml.is_some();
|
||||
let is_mdn = !mime_parser.mdn_reports.is_empty();
|
||||
let mut allow_creation = !is_mdn;
|
||||
let show_emails =
|
||||
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
|
||||
|
||||
let allow_creation;
|
||||
if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
|
||||
&& is_dc_message == MessengerMessage::No
|
||||
{
|
||||
@@ -470,10 +449,8 @@ async fn add_parts(
|
||||
allow_creation = false;
|
||||
}
|
||||
ShowEmails::AcceptedContacts => allow_creation = false,
|
||||
ShowEmails::All => allow_creation = !is_mdn,
|
||||
ShowEmails::All => {}
|
||||
}
|
||||
} else {
|
||||
allow_creation = !is_mdn;
|
||||
}
|
||||
|
||||
// check if the message introduces a new chat:
|
||||
@@ -482,44 +459,42 @@ async fn add_parts(
|
||||
// (of course, the user can add other chats manually later)
|
||||
let to_id: u32;
|
||||
|
||||
let state: MessageState;
|
||||
if incoming {
|
||||
state = if seen || fetching_existing_messages {
|
||||
MessageState::InSeen
|
||||
} else {
|
||||
MessageState::InFresh
|
||||
};
|
||||
to_id = DC_CONTACT_ID_SELF;
|
||||
|
||||
// Whether the message is a part of securejoin handshake that should be marked as seen
|
||||
// automatically.
|
||||
let securejoin_seen;
|
||||
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
|
||||
is_dc_message = MessengerMessage::Yes; // avoid discarding by show_emails setting
|
||||
chat_id = None;
|
||||
allow_creation = true;
|
||||
match handle_securejoin_handshake(context, mime_parser, from_id).await {
|
||||
Ok(securejoin::HandshakeMessage::Done) => {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
*hidden = true;
|
||||
*needs_delete_job = true;
|
||||
securejoin_seen = true;
|
||||
state = MessageState::InSeen;
|
||||
}
|
||||
Ok(securejoin::HandshakeMessage::Ignore) => {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
securejoin_seen = true;
|
||||
*hidden = true;
|
||||
state = MessageState::InSeen;
|
||||
}
|
||||
Ok(securejoin::HandshakeMessage::Propagate) => {
|
||||
// process messages as "member added" normally
|
||||
securejoin_seen = false;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Error in Secure-Join message handling: {}", err);
|
||||
return Ok(DC_CHAT_ID_TRASH);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
securejoin_seen = false;
|
||||
}
|
||||
|
||||
let test_normal_chat = if from_id == 0 {
|
||||
Default::default()
|
||||
} else {
|
||||
ChatIdBlocked::lookup_by_contact(context, from_id).await?
|
||||
};
|
||||
let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if chat_id.is_none() && mime_parser.failure_report.is_some() {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
@@ -530,7 +505,7 @@ async fn add_parts(
|
||||
// try to assign to a chat based on In-Reply-To/References:
|
||||
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) =
|
||||
lookup_chat_by_reply(context, mime_parser, &parent, from_id, to_ids).await?
|
||||
lookup_chat_by_reply(context, &mime_parser, &parent, from_id, to_ids).await?
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
@@ -550,7 +525,7 @@ async fn add_parts(
|
||||
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
|
||||
context,
|
||||
mime_parser,
|
||||
&mut mime_parser,
|
||||
sent_timestamp,
|
||||
if test_normal_chat.is_none() {
|
||||
allow_creation
|
||||
@@ -644,7 +619,7 @@ async fn add_parts(
|
||||
|
||||
if chat_id.is_none() {
|
||||
// try to create a normal chat
|
||||
let create_blocked = if from_id == DC_CONTACT_ID_SELF {
|
||||
let create_blocked = if from_id == to_id {
|
||||
Blocked::Not
|
||||
} else {
|
||||
Blocked::Request
|
||||
@@ -664,12 +639,11 @@ async fn add_parts(
|
||||
}
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
if chat_id_blocked != Blocked::Not {
|
||||
if chat_id_blocked != create_blocked {
|
||||
chat_id.set_blocked(context, create_blocked).await?;
|
||||
chat_id_blocked = create_blocked;
|
||||
}
|
||||
if create_blocked == Blocked::Request && parent.is_some() {
|
||||
if Blocked::Not != chat_id_blocked {
|
||||
if Blocked::Not == create_blocked {
|
||||
chat_id.unblock(context).await?;
|
||||
chat_id_blocked = Blocked::Not;
|
||||
} else if parent.is_some() {
|
||||
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
|
||||
// the contact requests will pop up and this should be just fine.
|
||||
Contact::scaleup_origin_by_id(context, from_id, Origin::IncomingReplyTo)
|
||||
@@ -686,12 +660,21 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
state =
|
||||
if seen || fetching_existing_messages || is_mdn || location_kml_is || securejoin_seen {
|
||||
MessageState::InSeen
|
||||
} else {
|
||||
MessageState::InFresh
|
||||
};
|
||||
// if the chat_id is blocked,
|
||||
// for unknown senders and non-delta-messages set the state to NOTICED
|
||||
// to not result in a chatlist-contact-request (this would require the state FRESH)
|
||||
if Blocked::Not != chat_id_blocked
|
||||
&& state == MessageState::InFresh
|
||||
&& !incoming_origin.is_known()
|
||||
&& is_dc_message == MessengerMessage::No
|
||||
&& show_emails != ShowEmails::All
|
||||
{
|
||||
state = MessageState::InNoticed;
|
||||
} else if fetching_existing_messages && Blocked::Request == chat_id_blocked {
|
||||
// The fetched existing message should be shown in the chatlist-contact-request because
|
||||
// a new user won't find the contact request in the menu
|
||||
state = MessageState::InFresh;
|
||||
}
|
||||
|
||||
let is_spam = (chat_id_blocked == Blocked::Request)
|
||||
&& !incoming_origin.is_known()
|
||||
@@ -709,28 +692,24 @@ async fn add_parts(
|
||||
state = MessageState::OutDelivered;
|
||||
to_id = to_ids.get_index(0).cloned().unwrap_or_default();
|
||||
|
||||
let self_sent = from_id == DC_CONTACT_ID_SELF
|
||||
&& to_ids.len() == 1
|
||||
&& to_ids.contains(&DC_CONTACT_ID_SELF);
|
||||
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
|
||||
is_dc_message = MessengerMessage::Yes; // avoid discarding by show_emails setting
|
||||
chat_id = None;
|
||||
allow_creation = true;
|
||||
match observe_securejoin_on_other_device(context, mime_parser, to_id).await {
|
||||
Ok(securejoin::HandshakeMessage::Done)
|
||||
| Ok(securejoin::HandshakeMessage::Ignore) => {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
*hidden = true;
|
||||
}
|
||||
Ok(securejoin::HandshakeMessage::Propagate) => {
|
||||
// process messages as "member added" normally
|
||||
chat_id = None;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Error in Secure-Join watching: {}", err);
|
||||
return Ok(DC_CHAT_ID_TRASH);
|
||||
}
|
||||
}
|
||||
} else if mime_parser.sync_items.is_some() && self_sent {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
}
|
||||
|
||||
// If the message is outgoing AND there is no Received header AND it's not in the sentbox,
|
||||
@@ -758,13 +737,14 @@ async fn add_parts(
|
||||
// Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them
|
||||
info!(context, "Email is probably just a draft (TRASH)");
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
allow_creation = false;
|
||||
}
|
||||
|
||||
if chat_id.is_none() {
|
||||
// try to assign to a chat based on In-Reply-To/References:
|
||||
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) =
|
||||
lookup_chat_by_reply(context, mime_parser, &parent, from_id, to_ids).await?
|
||||
lookup_chat_by_reply(context, &mime_parser, &parent, from_id, to_ids).await?
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
@@ -775,7 +755,7 @@ async fn add_parts(
|
||||
if chat_id.is_none() {
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
|
||||
context,
|
||||
mime_parser,
|
||||
&mut mime_parser,
|
||||
sent_timestamp,
|
||||
allow_creation,
|
||||
Blocked::Not,
|
||||
@@ -807,13 +787,16 @@ async fn add_parts(
|
||||
}
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
if chat_id_blocked != Blocked::Not && chat_id_blocked != create_blocked {
|
||||
chat_id.set_blocked(context, create_blocked).await?;
|
||||
chat_id_blocked = create_blocked;
|
||||
if Blocked::Not != chat_id_blocked && Blocked::Not == create_blocked {
|
||||
chat_id.unblock(context).await?;
|
||||
chat_id_blocked = Blocked::Not;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let self_sent = from_id == DC_CONTACT_ID_SELF
|
||||
&& to_ids.len() == 1
|
||||
&& to_ids.contains(&DC_CONTACT_ID_SELF);
|
||||
|
||||
if chat_id.is_none() && self_sent {
|
||||
// from_id==to_id==DC_CONTACT_ID_SELF - this is a self-sent messages,
|
||||
@@ -842,10 +825,6 @@ async fn add_parts(
|
||||
info!(context, "Existing non-decipherable message. (TRASH)");
|
||||
}
|
||||
|
||||
if is_mdn {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
}
|
||||
|
||||
let chat_id = chat_id.unwrap_or_else(|| {
|
||||
info!(context, "No chat id for message (TRASH)");
|
||||
DC_CHAT_ID_TRASH
|
||||
@@ -868,6 +847,8 @@ async fn add_parts(
|
||||
EphemeralTimer::Disabled
|
||||
};
|
||||
|
||||
let location_kml_is = mime_parser.location_kml.is_some();
|
||||
|
||||
// correct message_timestamp, it should not be used before,
|
||||
// however, we cannot do this earlier as we need chat_id to be set
|
||||
let in_fresh = state == MessageState::InFresh;
|
||||
@@ -875,11 +856,13 @@ async fn add_parts(
|
||||
|
||||
// Apply ephemeral timer changes to the chat.
|
||||
//
|
||||
// Only apply the timer when there are visible parts (e.g., the message does not consist only
|
||||
// of `location.kml` attachment). Timer changes without visible received messages may be
|
||||
// confusing to the user.
|
||||
if !chat_id.is_special()
|
||||
&& !mime_parser.parts.is_empty()
|
||||
// Only non-hidden timers are applied. Timers from hidden
|
||||
// messages such as read receipts can be useful to detect
|
||||
// ephemeral timer support, but timer changes without visible
|
||||
// received messages may be confusing to the user.
|
||||
if !*hidden
|
||||
&& !location_kml_is
|
||||
&& !is_mdn
|
||||
&& chat_id.get_ephemeral_timer(context).await? != ephemeral_timer
|
||||
{
|
||||
info!(
|
||||
@@ -972,35 +955,40 @@ async fn add_parts(
|
||||
warn!(context, "verification problem: {}", err);
|
||||
let s = format!("{}. See 'Info' for more details", err);
|
||||
mime_parser.repl_msg_by_error(s);
|
||||
} else {
|
||||
// change chat protection only when verification check passes
|
||||
if let Some(new_status) = new_status {
|
||||
if chat_id
|
||||
} else if let Some(new_status) = new_status {
|
||||
if !chat.is_protected()
|
||||
&& new_status == ProtectionStatus::Protected
|
||||
&& chat_id
|
||||
.update_timestamp(
|
||||
context,
|
||||
Param::ProtectionSettingsTimestamp,
|
||||
sent_timestamp,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
// Upgrade chat to a protected chat.
|
||||
// As this gives some guarantees to the user,
|
||||
// we do not allow downgrades over the wire.
|
||||
if let Err(e) = chat_id
|
||||
.inner_set_protection(context, ProtectionStatus::Protected)
|
||||
.await
|
||||
{
|
||||
if let Err(e) = chat_id.inner_set_protection(context, new_status).await {
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
chat_id,
|
||||
format!("Cannot set protection: {}", e),
|
||||
sort_timestamp,
|
||||
)
|
||||
.await?;
|
||||
return Ok(chat_id); // do not return an error as this would result in retrying the message
|
||||
}
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
chat_id,
|
||||
format!("Cannot set protection: {}", e),
|
||||
sort_timestamp,
|
||||
)
|
||||
.await?;
|
||||
return Ok(chat_id); // do not return an error as this would result in retrying the message
|
||||
}
|
||||
set_better_msg(
|
||||
mime_parser,
|
||||
context
|
||||
.stock_protection_msg(new_status, from_id as u32)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
set_better_msg(
|
||||
mime_parser,
|
||||
context
|
||||
.stock_protection_msg(new_status, from_id as u32)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1059,6 +1047,9 @@ async fn add_parts(
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let is_hidden = *hidden;
|
||||
|
||||
let mut is_hidden = is_hidden;
|
||||
let mut ids = Vec::with_capacity(parts.len());
|
||||
|
||||
let conn = context.sql.get_conn().await?;
|
||||
@@ -1073,7 +1064,7 @@ INSERT INTO msgs
|
||||
from_id, to_id, timestamp, timestamp_sent,
|
||||
timestamp_rcvd, type, state, msgrmsg,
|
||||
txt, subject, txt_raw, param,
|
||||
bytes, mime_headers, mime_in_reply_to,
|
||||
bytes, hidden, mime_headers, mime_in_reply_to,
|
||||
mime_references, mime_modified, error, ephemeral_timer,
|
||||
ephemeral_timestamp, download_state
|
||||
)
|
||||
@@ -1084,11 +1075,21 @@ INSERT INTO msgs
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?
|
||||
?, ?
|
||||
);
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let is_location_kml =
|
||||
location_kml_is && icnt == 1 && (part.msg == "-location-" || part.msg.is_empty());
|
||||
|
||||
if is_mdn || is_location_kml {
|
||||
is_hidden = true;
|
||||
if incoming {
|
||||
state = MessageState::InSeen; // Set the state to InSeen so that precheck_imf() adds a markseen job after we moved the message
|
||||
}
|
||||
}
|
||||
|
||||
let part_is_empty = part.msg.is_empty() && part.param.get(Param::Quote).is_none();
|
||||
let mime_modified = save_mime_modified && !part_is_empty;
|
||||
if mime_modified {
|
||||
@@ -1142,6 +1143,7 @@ INSERT INTO msgs
|
||||
part.param.to_string()
|
||||
},
|
||||
part.bytes as isize,
|
||||
is_hidden,
|
||||
if (save_mime_headers || mime_modified) && !trash {
|
||||
mime_headers.clone()
|
||||
} else {
|
||||
@@ -1166,8 +1168,11 @@ INSERT INTO msgs
|
||||
}
|
||||
drop(conn);
|
||||
|
||||
chat_id.unarchive(context).await?;
|
||||
if !is_hidden {
|
||||
chat_id.unarchive(context).await?;
|
||||
}
|
||||
|
||||
*hidden = is_hidden;
|
||||
created_db_entries.extend(ids.iter().map(|id| (chat_id, *id)));
|
||||
mime_parser.parts = parts;
|
||||
|
||||
@@ -1177,12 +1182,12 @@ INSERT INTO msgs
|
||||
);
|
||||
|
||||
// new outgoing message from another device marks the chat as noticed.
|
||||
if !incoming && !chat_id.is_special() {
|
||||
if !incoming && !*hidden && !chat_id.is_special() {
|
||||
chat::marknoticed_chat_if_older_than(context, chat_id, sort_timestamp).await?;
|
||||
}
|
||||
|
||||
// check event to send
|
||||
*create_event_to_send = if chat_id.is_trash() {
|
||||
*create_event_to_send = if chat_id.is_trash() || *hidden {
|
||||
None
|
||||
} else if incoming && state == MessageState::InFresh {
|
||||
Some(CreateEvent::IncomingMsg)
|
||||
@@ -1212,57 +1217,64 @@ INSERT INTO msgs
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
/// Saves attached locations to the database.
|
||||
///
|
||||
/// Emits an event if at least one new location was added.
|
||||
async fn save_locations(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
chat_id: ChatId,
|
||||
from_id: u32,
|
||||
msg_id: MsgId,
|
||||
) -> Result<()> {
|
||||
insert_msg_id: MsgId,
|
||||
hidden: bool,
|
||||
) {
|
||||
if chat_id.is_special() {
|
||||
// Do not save locations for trashed messages.
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
|
||||
let mut location_id_written = false;
|
||||
let mut send_event = false;
|
||||
|
||||
if let Some(message_kml) = &mime_parser.message_kml {
|
||||
if let Some(newest_location_id) =
|
||||
location::save(context, chat_id, from_id, &message_kml.locations, true).await?
|
||||
if mime_parser.message_kml.is_some() {
|
||||
let locations = &mime_parser.message_kml.as_ref().unwrap().locations;
|
||||
let newest_location_id = location::save(context, chat_id, from_id, locations, true)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if 0 != newest_location_id
|
||||
&& !hidden
|
||||
&& location::set_msg_location_id(context, insert_msg_id, newest_location_id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
location::set_msg_location_id(context, msg_id, newest_location_id).await?;
|
||||
location_id_written = true;
|
||||
send_event = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(location_kml) = &mime_parser.location_kml {
|
||||
if let Some(addr) = &location_kml.addr {
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
if contact.get_addr().to_lowercase() == addr.to_lowercase() {
|
||||
if let Some(newest_location_id) =
|
||||
location::save(context, chat_id, from_id, &location_kml.locations, false)
|
||||
.await?
|
||||
{
|
||||
location::set_msg_location_id(context, msg_id, newest_location_id).await?;
|
||||
if mime_parser.location_kml.is_some() {
|
||||
if let Some(ref addr) = mime_parser.location_kml.as_ref().unwrap().addr {
|
||||
if let Ok(contact) = Contact::get_by_id(context, from_id).await {
|
||||
if contact.get_addr().to_lowercase() == addr.to_lowercase() {
|
||||
let locations = &mime_parser.location_kml.as_ref().unwrap().locations;
|
||||
let newest_location_id =
|
||||
location::save(context, chat_id, from_id, locations, false)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if newest_location_id != 0 && !hidden && !location_id_written {
|
||||
if let Err(err) = location::set_msg_location_id(
|
||||
context,
|
||||
insert_msg_id,
|
||||
newest_location_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!(context, "Failed to set msg_location_id: {:?}", err);
|
||||
}
|
||||
}
|
||||
send_event = true;
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Address in location.kml {:?} is not the same as the sender address {:?}.",
|
||||
addr,
|
||||
contact.get_addr()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if send_event {
|
||||
context.emit_event(EventType::LocationChanged(Some(from_id)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn calc_sort_timestamp(
|
||||
@@ -1296,7 +1308,7 @@ async fn calc_sort_timestamp(
|
||||
|
||||
async fn lookup_chat_by_reply(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
mime_parser: &&mut MimeMessage,
|
||||
parent: &Option<Message>,
|
||||
from_id: u32,
|
||||
to_ids: &ContactIds,
|
||||
@@ -1513,17 +1525,6 @@ async fn create_or_lookup_group(
|
||||
}
|
||||
set_better_msg(mime_parser, &better_msg);
|
||||
|
||||
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
|
||||
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await {
|
||||
warn!(context, "verification problem: {}", err);
|
||||
let s = format!("{}. See 'Info' for more details", err);
|
||||
mime_parser.repl_msg_by_error(&s);
|
||||
}
|
||||
ProtectionStatus::Protected
|
||||
} else {
|
||||
ProtectionStatus::Unprotected
|
||||
};
|
||||
|
||||
// check if the group does not exist but should be created
|
||||
let group_explicitly_left = chat::is_group_explicitly_left(context, &grpid)
|
||||
.await
|
||||
@@ -1544,6 +1545,18 @@ async fn create_or_lookup_group(
|
||||
|| X_MrAddToGrp.is_some() && addr_cmp(&self_addr, X_MrAddToGrp.as_ref().unwrap()))
|
||||
{
|
||||
// group does not exist but should be created
|
||||
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
|
||||
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await
|
||||
{
|
||||
warn!(context, "verification problem: {}", err);
|
||||
let s = format!("{}. See 'Info' for more details", err);
|
||||
mime_parser.repl_msg_by_error(&s);
|
||||
}
|
||||
ProtectionStatus::Protected
|
||||
} else {
|
||||
ProtectionStatus::Unprotected
|
||||
};
|
||||
|
||||
if !allow_creation {
|
||||
info!(context, "creating group forbidden by caller");
|
||||
return Ok(None);
|
||||
@@ -1584,16 +1597,6 @@ async fn create_or_lookup_group(
|
||||
// .add_protection_msg(context, ProtectionStatus::Protected, false, 0)
|
||||
// .await?;
|
||||
//}
|
||||
} else if let Some(chat_id) = chat_id {
|
||||
if create_protected == ProtectionStatus::Protected {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if !chat.is_protected() {
|
||||
chat_id
|
||||
.inner_set_protection(context, ProtectionStatus::Protected)
|
||||
.await?;
|
||||
recreate_member_list = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// again, check chat_id
|
||||
@@ -2065,7 +2068,7 @@ async fn check_verified_properties(
|
||||
let peerstate = Peerstate::from_addr(context, &to_addr).await?;
|
||||
|
||||
// mark gossiped keys (if any) as verified
|
||||
if mimeparser.gossiped_addr.contains(&to_addr) {
|
||||
if mimeparser.gossipped_addr.contains(&to_addr) {
|
||||
if let Some(mut peerstate) = peerstate {
|
||||
// if we're here, we know the gossip key is verified:
|
||||
// - use the gossip-key as verified-key if there is no verified-key
|
||||
|
||||
@@ -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,18 +844,22 @@ mod tests {
|
||||
#[test]
|
||||
fn test_dc_truncate(
|
||||
buf: String,
|
||||
approx_chars in 0..100usize
|
||||
approx_chars in 0..10000usize
|
||||
) {
|
||||
let res = dc_truncate(&buf, approx_chars);
|
||||
let el_len = 5;
|
||||
let l = res.chars().count();
|
||||
assert!(
|
||||
l <= approx_chars + el_len,
|
||||
"buf: '{}' - res: '{}' - len {}, approx {}",
|
||||
&buf, &res, res.len(), approx_chars
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
if buf.chars().count() > approx_chars + el_len {
|
||||
if approx_chars > 0 && buf.chars().count() > approx_chars + el_len {
|
||||
let l = res.len();
|
||||
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
|
||||
}
|
||||
@@ -982,7 +986,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_filemeta() {
|
||||
let (w, h) = dc_get_filemeta(test_utils::AVATAR_900x900_BYTES).unwrap();
|
||||
let data = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
let (w, h) = dc_get_filemeta(data).unwrap();
|
||||
assert_eq!(w, 900);
|
||||
assert_eq!(h, 900);
|
||||
|
||||
|
||||
@@ -323,7 +323,4 @@ pub enum EventType {
|
||||
/// dc_get_connectivity_html() for details.
|
||||
#[strum(props(id = "2100"))]
|
||||
ConnectivityChanged,
|
||||
|
||||
#[strum(props(id = "2110"))]
|
||||
SelfavatarChanged,
|
||||
}
|
||||
|
||||
15
src/job.rs
15
src/job.rs
@@ -433,13 +433,6 @@ 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(())
|
||||
}
|
||||
})
|
||||
@@ -991,7 +984,7 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
|
||||
}
|
||||
|
||||
if rendered_msg.is_gossiped {
|
||||
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
|
||||
chat::set_gossiped_timestamp(context, msg.chat_id, time()).await?;
|
||||
}
|
||||
|
||||
if 0 != rendered_msg.last_added_location_id {
|
||||
@@ -1008,12 +1001,6 @@ 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);
|
||||
|
||||
@@ -82,7 +82,6 @@ pub mod securejoin;
|
||||
mod simplify;
|
||||
mod smtp;
|
||||
pub mod stock_str;
|
||||
mod sync;
|
||||
mod token;
|
||||
mod update_helper;
|
||||
#[macro_use]
|
||||
|
||||
@@ -552,20 +552,17 @@ pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Saves given locations to the database.
|
||||
///
|
||||
/// Returns the database row ID of the location with the highest timestamp.
|
||||
pub(crate) async fn save(
|
||||
pub async fn save(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
locations: &[Location],
|
||||
independent: bool,
|
||||
) -> Result<Option<u32>> {
|
||||
) -> Result<u32> {
|
||||
ensure!(!chat_id.is_special(), "Invalid chat id");
|
||||
|
||||
let mut newest_timestamp = 0;
|
||||
let mut newest_location_id = None;
|
||||
let mut newest_location_id = 0;
|
||||
|
||||
let stmt_insert = "INSERT INTO locations\
|
||||
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
|
||||
@@ -603,12 +600,12 @@ pub(crate) async fn save(
|
||||
drop(stmt_test);
|
||||
drop(stmt_insert);
|
||||
newest_timestamp = timestamp;
|
||||
newest_location_id = Some(u32::try_from(conn.last_insert_rowid())?);
|
||||
newest_location_id = conn.last_insert_rowid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(newest_location_id)
|
||||
Ok(u32::try_from(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,7 +1,6 @@
|
||||
//! # Logging.
|
||||
|
||||
use crate::context::Context;
|
||||
use async_std::task::block_on;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! info {
|
||||
@@ -40,28 +39,10 @@ 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,
|
||||
@@ -153,31 +134,3 @@ 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,10 +11,8 @@ 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)]
|
||||
@@ -370,18 +368,8 @@ 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().add_root_certificate(LETSENCRYPT_ROOT.clone());
|
||||
let tls_builder = async_native_tls::TlsConnector::new();
|
||||
|
||||
if strict_tls {
|
||||
tls_builder
|
||||
@@ -442,13 +430,4 @@ 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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,13 +68,6 @@ 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,
|
||||
}
|
||||
|
||||
@@ -87,12 +80,6 @@ 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,
|
||||
@@ -218,7 +205,6 @@ impl<'a> MimeFactory<'a> {
|
||||
references,
|
||||
req_mdn,
|
||||
last_added_location_id: 0,
|
||||
sync_ids_to_delete: None,
|
||||
attach_selfavatar,
|
||||
};
|
||||
Ok(factory)
|
||||
@@ -263,7 +249,6 @@ impl<'a> MimeFactory<'a> {
|
||||
references: String::default(),
|
||||
req_mdn: false,
|
||||
last_added_location_id: 0,
|
||||
sync_ids_to_delete: None,
|
||||
attach_selfavatar: false,
|
||||
};
|
||||
|
||||
@@ -361,7 +346,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.id.get_gossiped_timestamp(context).await?;
|
||||
let gossiped_timestamp = chat.get_gossiped_timestamp(context).await?;
|
||||
if time() > gossiped_timestamp + (2 * 24 * 60 * 60) {
|
||||
Ok(true)
|
||||
} else {
|
||||
@@ -618,20 +603,12 @@ impl<'a> MimeFactory<'a> {
|
||||
main_part
|
||||
} else {
|
||||
// Multiple parts, render as multipart.
|
||||
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())
|
||||
})
|
||||
parts.into_iter().fold(
|
||||
PartBuilder::new()
|
||||
.message_type(MimeMultipartType::Mixed)
|
||||
.child(main_part.build()),
|
||||
|message, part| message.child(part.build()),
|
||||
)
|
||||
};
|
||||
|
||||
let outer_message = if is_encrypted {
|
||||
@@ -752,7 +729,6 @@ 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,
|
||||
})
|
||||
@@ -897,7 +873,7 @@ impl<'a> MimeFactory<'a> {
|
||||
"ephemeral-timer-changed".to_string(),
|
||||
));
|
||||
}
|
||||
SystemMessage::LocationOnly | SystemMessage::MultiDeviceSync => {
|
||||
SystemMessage::LocationOnly => {
|
||||
// This should prevent automatic replies,
|
||||
// such as non-delivery reports.
|
||||
//
|
||||
@@ -1127,15 +1103,6 @@ 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) {
|
||||
|
||||
@@ -28,7 +28,6 @@ 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.
|
||||
///
|
||||
@@ -57,14 +56,11 @@ pub struct MimeMessage {
|
||||
/// this set is empty.
|
||||
pub signatures: HashSet<Fingerprint>,
|
||||
|
||||
/// 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 gossipped_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>,
|
||||
@@ -128,10 +124,6 @@ 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 {
|
||||
@@ -200,7 +192,7 @@ impl MimeMessage {
|
||||
|
||||
// Memory location for a possible decrypted message.
|
||||
let mut mail_raw = Vec::new();
|
||||
let mut gossiped_addr = Default::default();
|
||||
let mut gossipped_addr = Default::default();
|
||||
|
||||
let (mail, signatures, warn_empty_signature) =
|
||||
match e2ee::try_decrypt(context, &mail, message_time).await {
|
||||
@@ -223,7 +215,7 @@ impl MimeMessage {
|
||||
if !signatures.is_empty() {
|
||||
let gossip_headers =
|
||||
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
|
||||
gossiped_addr = update_gossip_peerstates(
|
||||
gossipped_addr = update_gossip_peerstates(
|
||||
context,
|
||||
message_time,
|
||||
&mail,
|
||||
@@ -281,13 +273,12 @@ impl MimeMessage {
|
||||
|
||||
// only non-empty if it was a valid autocrypt message
|
||||
signatures,
|
||||
gossiped_addr,
|
||||
gossipped_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,
|
||||
@@ -822,12 +813,6 @@ 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
|
||||
@@ -1014,20 +999,7 @@ 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 */
|
||||
|
||||
@@ -1382,9 +1354,6 @@ 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,
|
||||
@@ -1392,46 +1361,42 @@ async fn update_gossip_peerstates(
|
||||
gossip_headers: Vec<String>,
|
||||
) -> Result<HashSet<String>> {
|
||||
// XXX split the parsing from the modification part
|
||||
let mut gossiped_addr: HashSet<String> = Default::default();
|
||||
let mut gossipped_addr: HashSet<String> = Default::default();
|
||||
|
||||
for value in &gossip_headers {
|
||||
let header = match value.parse::<Aheader>() {
|
||||
Ok(header) => header,
|
||||
Err(err) => {
|
||||
warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
|
||||
continue;
|
||||
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,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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(gossiped_addr)
|
||||
Ok(gossipped_addr)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -4,7 +4,7 @@ mod data;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS, PROVIDER_UPDATED};
|
||||
use async_std_resolver::resolver_from_system_conf;
|
||||
use async_std_resolver::{config, resolver};
|
||||
use chrono::{NaiveDateTime, NaiveTime};
|
||||
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)]
|
||||
@@ -118,7 +118,12 @@ 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_from_system_conf().await {
|
||||
if let Ok(resolver) = resolver(
|
||||
config::ResolverConfig::default(),
|
||||
config::ResolverOpts::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
let mut fqdn: String = domain.to_string();
|
||||
if !fqdn.ends_with('.') {
|
||||
fqdn.push('.');
|
||||
|
||||
12
src/qr.rs
12
src/qr.rs
@@ -391,10 +391,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
} => {
|
||||
token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?;
|
||||
token::delete(context, token::Namespace::Auth, &authcode).await?;
|
||||
context
|
||||
.sync_qr_code_token_deletion(invitenumber, authcode)
|
||||
.await?;
|
||||
context.send_sync_msg().await?;
|
||||
}
|
||||
Qr::WithdrawVerifyGroup {
|
||||
invitenumber,
|
||||
@@ -403,10 +399,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
} => {
|
||||
token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?;
|
||||
token::delete(context, token::Namespace::Auth, &authcode).await?;
|
||||
context
|
||||
.sync_qr_code_token_deletion(invitenumber, authcode)
|
||||
.await?;
|
||||
context.send_sync_msg().await?;
|
||||
}
|
||||
Qr::ReviveVerifyContact {
|
||||
invitenumber,
|
||||
@@ -415,8 +407,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
} => {
|
||||
token::save(context, token::Namespace::InviteNumber, None, &invitenumber).await?;
|
||||
token::save(context, token::Namespace::Auth, None, &authcode).await?;
|
||||
context.sync_qr_code_tokens(None).await?;
|
||||
context.send_sync_msg().await?;
|
||||
}
|
||||
Qr::ReviveVerifyGroup {
|
||||
invitenumber,
|
||||
@@ -435,8 +425,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
token::save(context, token::Namespace::Auth, chat_id, &authcode).await?;
|
||||
context.sync_qr_code_tokens(chat_id).await?;
|
||||
context.send_sync_msg().await?;
|
||||
}
|
||||
_ => bail!("qr code {:?} does not contain config", qr),
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
//! 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::{bail, Context as _, Error, Result};
|
||||
use anyhow::{anyhow, bail, Context as _, Error, Result};
|
||||
use async_std::channel::Receiver;
|
||||
use async_std::sync::Mutex;
|
||||
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{self, is_contact_in_chat, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
|
||||
use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
|
||||
use crate::contact::{Contact, Origin, VerifiedStatus};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
@@ -28,7 +30,6 @@ use crate::token;
|
||||
mod bobstate;
|
||||
mod qrinvite;
|
||||
|
||||
use crate::token::Namespace;
|
||||
use bobstate::{BobHandshakeStage, BobState, BobStateHandle};
|
||||
use qrinvite::QrInvite;
|
||||
|
||||
@@ -79,8 +80,8 @@ enum StartedProtocolVariant {
|
||||
SetupContact,
|
||||
/// The secure-join protocol, to join a group.
|
||||
SecureJoin {
|
||||
ongoing_receiver: Receiver<()>,
|
||||
group_id: String,
|
||||
group_name: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -108,14 +109,16 @@ impl Bob {
|
||||
*guard = None;
|
||||
}
|
||||
let variant = match invite {
|
||||
QrInvite::Group {
|
||||
ref grpid,
|
||||
ref name,
|
||||
..
|
||||
} => StartedProtocolVariant::SecureJoin {
|
||||
group_id: grpid.clone(),
|
||||
group_name: name.clone(),
|
||||
},
|
||||
QrInvite::Group { ref grpid, .. } => {
|
||||
let receiver = context
|
||||
.alloc_ongoing()
|
||||
.await
|
||||
.map_err(|_| JoinError::OngoingRunning)?;
|
||||
StartedProtocolVariant::SecureJoin {
|
||||
ongoing_receiver: receiver,
|
||||
group_id: grpid.clone(),
|
||||
}
|
||||
}
|
||||
_ => StartedProtocolVariant::SetupContact,
|
||||
};
|
||||
match BobState::start_protocol(context, invite).await {
|
||||
@@ -168,11 +171,8 @@ 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 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 invitenumber = token::lookup_or_new(context, token::Namespace::InviteNumber, group).await;
|
||||
let auth = token::lookup_or_new(context, token::Namespace::Auth, group).await;
|
||||
let self_addr = match context.get_config(Config::ConfiguredAddr).await {
|
||||
Ok(Some(addr)) => addr,
|
||||
Ok(None) => {
|
||||
@@ -208,9 +208,7 @@ 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(),
|
||||
@@ -222,9 +220,6 @@ 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(),
|
||||
@@ -276,7 +271,9 @@ pub enum JoinError {
|
||||
/// This is the start of the process for the joiner. See the module and ffi documentation
|
||||
/// for more details.
|
||||
///
|
||||
/// The function returns immediately and the handshake will run in background.
|
||||
/// 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.
|
||||
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);
|
||||
@@ -307,34 +304,39 @@ 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, 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?
|
||||
// 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;
|
||||
};
|
||||
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)
|
||||
context.free_ongoing().await;
|
||||
Ok(chatid)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,13 +349,13 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
#[error("Failed sending handshake message")]
|
||||
pub struct SendMsgError(#[from] anyhow::Error);
|
||||
|
||||
/// Send handshake message from Alice's device;
|
||||
/// Bob's handshake messages are sent in `BobState::send_handshake_message()`.
|
||||
async fn send_alice_handshake_msg(
|
||||
async fn send_handshake_msg(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
contact_chat_id: ChatId,
|
||||
step: &str,
|
||||
param2: &str,
|
||||
fingerprint: Option<Fingerprint>,
|
||||
grpid: &str,
|
||||
) -> Result<(), SendMsgError> {
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
@@ -362,55 +364,67 @@ async fn send_alice_handshake_msg(
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
|
||||
msg.param.set(Param::Arg, step);
|
||||
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);
|
||||
}
|
||||
if let Some(fp) = fingerprint {
|
||||
msg.param.set(Param::Arg3, fp.hex());
|
||||
}
|
||||
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?;
|
||||
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?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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 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)
|
||||
}
|
||||
}
|
||||
|
||||
async fn fingerprint_equals_sender(
|
||||
context: &Context,
|
||||
fingerprint: &Fingerprint,
|
||||
contact_id: u32,
|
||||
contact_chat_id: ChatId,
|
||||
) -> Result<bool, Error> {
|
||||
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 [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);
|
||||
}
|
||||
};
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -471,6 +485,21 @@ 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() {
|
||||
@@ -499,18 +528,14 @@ 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_alice_handshake_msg(
|
||||
send_handshake_msg(
|
||||
context,
|
||||
contact_id,
|
||||
contact_chat_id,
|
||||
&format!("{}-auth-required", &step[..2]),
|
||||
"",
|
||||
None,
|
||||
"",
|
||||
)
|
||||
.await?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
@@ -523,19 +548,11 @@ 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,
|
||||
contact_id,
|
||||
bobstate.chat_id(context).await?,
|
||||
why,
|
||||
)
|
||||
.await?;
|
||||
could_not_establish_secure_connection(context, bobstate.chat_id(), why)
|
||||
.await?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
Some(_stage) => {
|
||||
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)
|
||||
}
|
||||
@@ -558,8 +575,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
None => {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
contact_chat_id,
|
||||
"Fingerprint not provided.",
|
||||
)
|
||||
.await?;
|
||||
@@ -569,18 +585,16 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
contact_chat_id,
|
||||
"Auth not encrypted.",
|
||||
)
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if !fingerprint_equals_sender(context, &fingerprint, contact_id).await? {
|
||||
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await? {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on inviter-side.",
|
||||
)
|
||||
.await?;
|
||||
@@ -593,8 +607,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
None => {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
contact_chat_id,
|
||||
"Auth not provided.",
|
||||
)
|
||||
.await?;
|
||||
@@ -602,20 +615,14 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
}
|
||||
};
|
||||
if !token::exists(context, token::Namespace::Auth, auth_0).await {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Auth invalid.",
|
||||
)
|
||||
.await?;
|
||||
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.")
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on inviter-side.",
|
||||
)
|
||||
.await?;
|
||||
@@ -623,6 +630,7 @@ 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 {
|
||||
@@ -638,7 +646,6 @@ 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
|
||||
@@ -650,17 +657,13 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
}
|
||||
} else {
|
||||
// Alice -> Bob
|
||||
secure_connection_established(
|
||||
send_handshake_msg(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
)
|
||||
.await?;
|
||||
send_alice_handshake_msg(
|
||||
context,
|
||||
contact_id,
|
||||
contact_chat_id,
|
||||
"vc-contact-confirm",
|
||||
"",
|
||||
Some(fingerprint),
|
||||
"",
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -682,23 +685,13 @@ 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,
|
||||
contact_id,
|
||||
bobstate.chat_id(context).await?,
|
||||
why,
|
||||
)
|
||||
.await?;
|
||||
could_not_establish_secure_connection(context, bobstate.chat_id(), why)
|
||||
.await?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
Some(BobHandshakeStage::Completed) => {
|
||||
// Can only be BobHandshakeStage::Completed
|
||||
secure_connection_established(
|
||||
context,
|
||||
contact_id,
|
||||
bobstate.chat_id(context).await?,
|
||||
)
|
||||
.await?;
|
||||
secure_connection_established(context, bobstate.chat_id()).await?;
|
||||
Ok(retval)
|
||||
}
|
||||
Some(_) => {
|
||||
@@ -782,6 +775,21 @@ 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"
|
||||
@@ -794,8 +802,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
contact_chat_id,
|
||||
"Message not encrypted correctly.",
|
||||
)
|
||||
.await?;
|
||||
@@ -807,8 +814,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
None => {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
contact_chat_id,
|
||||
"Fingerprint not provided, please update Delta Chat on all your devices.",
|
||||
)
|
||||
.await?;
|
||||
@@ -818,8 +824,7 @@ 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_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
contact_chat_id,
|
||||
format!("Fingerprint mismatch on observing {}.", step).as_ref(),
|
||||
)
|
||||
.await?;
|
||||
@@ -837,22 +842,30 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
|
||||
async fn secure_connection_established(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
chat_id: ChatId,
|
||||
contact_chat_id: ChatId,
|
||||
) -> Result<(), Error> {
|
||||
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));
|
||||
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");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn could_not_establish_secure_connection(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
chat_id: ChatId,
|
||||
contact_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,
|
||||
@@ -863,11 +876,13 @@ async fn could_not_establish_secure_connection(
|
||||
},
|
||||
)
|
||||
.await;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
|
||||
chat::add_info_msg(context, contact_chat_id, &msg, time()).await?;
|
||||
error!(
|
||||
context,
|
||||
"StockMessage::ContactNotVerified posted to 1:1 chat ({})", details
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -931,19 +946,15 @@ 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);
|
||||
@@ -962,7 +973,6 @@ 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);
|
||||
@@ -974,7 +984,6 @@ 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;
|
||||
@@ -1051,11 +1060,6 @@ 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;
|
||||
@@ -1317,8 +1321,6 @@ 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);
|
||||
@@ -1340,11 +1342,15 @@ mod tests {
|
||||
.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);
|
||||
// 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() })
|
||||
};
|
||||
|
||||
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());
|
||||
@@ -1458,24 +1464,10 @@ 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::{bail, Error, Result};
|
||||
use anyhow::{Error, Result};
|
||||
use async_std::sync::MutexGuard;
|
||||
|
||||
use crate::chat::{self, ChatId};
|
||||
use crate::constants::{Blocked, Viewtype};
|
||||
use crate::constants::Viewtype;
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
@@ -67,18 +67,9 @@ impl<'a> BobStateHandle<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 the [`ChatId`] of the 1:1 chat with the inviter (Alice).
|
||||
pub fn chat_id(&self) -> ChatId {
|
||||
self.bobstate.chat_id
|
||||
}
|
||||
|
||||
/// Returns a reference to the [`QrInvite`] of the joiner process.
|
||||
@@ -194,11 +185,10 @@ impl BobState {
|
||||
context: &Context,
|
||||
invite: QrInvite,
|
||||
) -> Result<(Self, BobHandshakeStage), JoinError> {
|
||||
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? {
|
||||
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? {
|
||||
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
|
||||
info!(context, "Taking securejoin protocol shortcut");
|
||||
let state = Self {
|
||||
@@ -307,9 +297,7 @@ impl BobState {
|
||||
self.next = SecureJoinStep::Terminated;
|
||||
return Ok(Some(BobHandshakeStage::Terminated(reason)));
|
||||
}
|
||||
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.invite.contact_id())
|
||||
.await?
|
||||
{
|
||||
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await? {
|
||||
self.next = SecureJoinStep::Terminated;
|
||||
return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch")));
|
||||
}
|
||||
|
||||
@@ -487,16 +487,6 @@ 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,
|
||||
|
||||
@@ -276,15 +276,6 @@ 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,
|
||||
|
||||
@@ -326,13 +317,6 @@ 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 {
|
||||
@@ -598,32 +582,6 @@ 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)
|
||||
@@ -666,16 +624,6 @@ 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)
|
||||
|
||||
506
src/sync.rs
506
src/sync.rs
@@ -1,506 +0,0 @@
|
||||
//! # 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 itertools::Itertools;
|
||||
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()).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,9 +34,6 @@ 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;
|
||||
|
||||
@@ -111,15 +108,12 @@ 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
|
||||
// (but not if there is a panic on another thread)
|
||||
let looptask_id = task::current().id();
|
||||
// Make sure that the test fails if there is a panic on this thread here:
|
||||
let current_id = task::current().id();
|
||||
let orig_hook = panic::take_hook();
|
||||
panic::set_hook(Box::new(move |panic_info| {
|
||||
if let Some(panicked_task) = task::try_current() {
|
||||
if panicked_task.id() == looptask_id {
|
||||
poison_sender.try_send(panic_info.to_string()).ok();
|
||||
}
|
||||
if task::current().id() == current_id {
|
||||
poison_sender.try_send(panic_info.to_string()).ok();
|
||||
}
|
||||
orig_hook(panic_info);
|
||||
}));
|
||||
@@ -593,21 +587,6 @@ 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