diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 9e759ce0b..511d70b57 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -4925,8 +4925,10 @@ void dc_event_unref(dc_event_t* event); #define DC_STR_VIDEOCHAT_INVITATION 82 #define DC_STR_VIDEOCHAT_INVITE_MSG_BODY 83 #define DC_STR_CONFIGURATION_FAILED 84 +#define DC_STR_BAD_TIME_MSG_BODY 85 +#define DC_STR_UPDATE_REMINDER_MSG_BODY 86 -#define DC_STR_COUNT 84 +#define DC_STR_COUNT 86 /* * @} diff --git a/src/chat.rs b/src/chat.rs index 8ba0cc553..f6e097171 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2756,14 +2756,35 @@ pub async fn add_device_msg_with_importance( prepare_msg_blob(context, msg).await?; chat_id.unarchive(context).await?; + let timestamp_sent = dc_create_smeared_timestamp(context).await; + + // makes sure, the added message is the last one, + // even if the date is wrong (useful esp. when warning about bad dates) + let mut timestamp_sort = timestamp_sent; + if let Some(last_msg_time) = context + .sql + .query_get_value( + context, + "SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", + paramsv![chat_id], + ) + .await + { + if timestamp_sort <= last_msg_time { + timestamp_sort = last_msg_time + 1; + } + } + context.sql.execute( - "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,param,rfc724_mid) \ - VALUES (?,?,?, ?,?,?, ?,?,?);", + "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,timestamp_sent,timestamp_rcvd,type,state, txt,param,rfc724_mid) \ + VALUES (?,?,?, ?,?,?,?,?, ?,?,?);", paramsv![ chat_id, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF, - dc_create_smeared_timestamp(context).await, + timestamp_sort, + timestamp_sent, + timestamp_sent, // timestamp_sent equals timestamp_rcvd msg.viewtype, MessageState::InFresh, msg.text.as_ref().cloned().unwrap_or_default(), diff --git a/src/constants.rs b/src/constants.rs index 7cb9130fb..58ec42715 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -108,6 +108,12 @@ pub const DC_GCL_ADD_SELF: usize = 0x02; // unchanged user avatars are resent to the recipients every some days pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14; +// warn about an outdated app after a given number of days. +// as we use the "provider-db generation date" as reference (that might not be updated very often) +// and as not all system get speedy updates, +// do not use too small value that will annoy users checking for nonexistant updates. +pub const DC_OUTDATED_WARNING_DAYS: i64 = 365; + /// virtual chat showing all messages belonging to chats flagged with chats.blocked=2 pub const DC_CHAT_ID_DEADDROP: u32 = 1; /// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again) diff --git a/src/dc_tools.rs b/src/dc_tools.rs index ad22ac6f8..0551418dc 100644 --- a/src/dc_tools.rs +++ b/src/dc_tools.rs @@ -15,9 +15,14 @@ use async_std::{fs, io}; use chrono::{Local, TimeZone}; use rand::{thread_rng, Rng}; +use crate::chat::{add_device_msg, add_device_msg_with_importance}; +use crate::constants::{Viewtype, DC_OUTDATED_WARNING_DAYS}; use crate::context::Context; use crate::error::{bail, Error}; use crate::events::EventType; +use crate::message::Message; +use crate::provider::get_provider_update_timestamp; +use crate::stock::StockMessage; /// Shortens a string to a specified length and adds "[...]" to the /// end of the shortened string. @@ -151,6 +156,73 @@ pub(crate) async fn dc_create_smeared_timestamps(context: &Context, count: usize start } +// if the system time is not plausible, once a day, add a device message. +// for testing we're using time() as that is also used for message timestamps. +// moreover, add a warning if the app is outdated. +pub(crate) async fn maybe_add_time_based_warnings(context: &Context) { + if !maybe_warn_on_bad_time(context, time(), get_provider_update_timestamp()).await { + maybe_warn_on_outdated(context, time(), get_provider_update_timestamp()).await; + } +} + +async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestamp: i64) -> bool { + if now < known_past_timestamp { + let mut msg = Message::new(Viewtype::Text); + msg.text = Some( + context + .stock_string_repl_str( + StockMessage::BadTimeMsgBody, + Local + .timestamp(now, 0) + .format("%Y-%m-%d %H:%M:%S") + .to_string(), + ) + .await, + ); + add_device_msg_with_importance( + context, + Some( + format!( + "bad-time-warning-{}", + chrono::NaiveDateTime::from_timestamp(now, 0).format("%Y-%m-%d") // repeat every day + ) + .as_str(), + ), + Some(&mut msg), + true, + ) + .await + .ok(); + return true; + } + false +} + +async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time: i64) { + if now > approx_compile_time + DC_OUTDATED_WARNING_DAYS * 24 * 60 * 60 { + let mut msg = Message::new(Viewtype::Text); + msg.text = Some( + context + .stock_str(StockMessage::UpdateReminderMsgBody) + .await + .into(), + ); + add_device_msg( + context, + Some( + format!( + "outdated-warning-{}", + chrono::NaiveDateTime::from_timestamp(now, 0).format("%Y-%m") // repeat every month + ) + .as_str(), + ), + Some(&mut msg), + ) + .await + .ok(); + } +} + /* Message-ID tools */ pub(crate) fn dc_create_id() -> String { /* generate an id. the generated ID should be as short and as unique as possible: @@ -800,6 +872,9 @@ mod tests { assert_eq!("@d.tt".parse::().is_ok(), false); } + use crate::chat; + use crate::chatlist::Chatlist; + use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; use proptest::prelude::*; proptest! { @@ -993,4 +1068,123 @@ mod tests { assert_eq!(improve_single_line_input("Hi\naiae "), "Hi aiae"); assert_eq!(improve_single_line_input("\r\nahte\n\r"), "ahte"); } + + #[async_std::test] + async fn test_maybe_warn_on_bad_time() { + let t = TestContext::new().await; + let timestamp_now = time(); + let timestamp_future = timestamp_now + 60 * 60 * 24 * 7; + let timestamp_past = NaiveDateTime::new( + NaiveDate::from_ymd(2020, 9, 1), + NaiveTime::from_hms(0, 0, 0), + ) + .timestamp_millis() + / 1_000; + + // a correct time must not add a device message + maybe_warn_on_bad_time(&t.ctx, timestamp_now, get_provider_update_timestamp()).await; + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + // we cannot find out if a date in the future is wrong - a device message is not added + maybe_warn_on_bad_time(&t.ctx, timestamp_future, get_provider_update_timestamp()).await; + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + // a date in the past must add a device message + maybe_warn_on_bad_time(&t.ctx, timestamp_past, get_provider_update_timestamp()).await; + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let device_chat_id = chats.get_chat_id(0); + let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await; + assert_eq!(msgs.len(), 1); + + // the message should be added only once a day - test that an hour later and nearly a day later + maybe_warn_on_bad_time( + &t.ctx, + timestamp_past + 60 * 60, + get_provider_update_timestamp(), + ) + .await; + let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await; + assert_eq!(msgs.len(), 1); + + maybe_warn_on_bad_time( + &t.ctx, + timestamp_past + 60 * 60 * 24 - 1, + get_provider_update_timestamp(), + ) + .await; + let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await; + assert_eq!(msgs.len(), 1); + + // next day, there should be another device message + maybe_warn_on_bad_time( + &t.ctx, + timestamp_past + 60 * 60 * 24, + get_provider_update_timestamp(), + ) + .await; + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + assert_eq!(device_chat_id, chats.get_chat_id(0)); + let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await; + assert_eq!(msgs.len(), 2); + } + + #[async_std::test] + async fn test_maybe_warn_on_outdated() { + let t = TestContext::new().await; + let timestamp_now: i64 = time(); + + // in about 6 months, the app should not be outdated + // (if this fails, provider-db is not updated since 6 months) + maybe_warn_on_outdated( + &t.ctx, + timestamp_now + 180 * 24 * 60 * 60, + get_provider_update_timestamp(), + ) + .await; + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + // in 1 year, the app should be considered as outdated + maybe_warn_on_outdated( + &t.ctx, + timestamp_now + 365 * 24 * 60 * 60, + get_provider_update_timestamp(), + ) + .await; + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let device_chat_id = chats.get_chat_id(0); + let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await; + assert_eq!(msgs.len(), 1); + + // do not repeat the warning every day ... + maybe_warn_on_outdated( + &t.ctx, + timestamp_now + (365 + 1) * 24 * 60 * 60, + get_provider_update_timestamp(), + ) + .await; + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let device_chat_id = chats.get_chat_id(0); + let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await; + assert_eq!(msgs.len(), 1); + + // ... but every month + maybe_warn_on_outdated( + &t.ctx, + timestamp_now + (365 + 31) * 24 * 60 * 60, + get_provider_update_timestamp(), + ) + .await; + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let device_chat_id = chats.get_chat_id(0); + let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await; + assert_eq!(msgs.len(), 2); + } } diff --git a/src/provider/data.rs b/src/provider/data.rs index aa24c185a..35a5fc49c 100644 --- a/src/provider/data.rs +++ b/src/provider/data.rs @@ -807,4 +807,6 @@ lazy_static::lazy_static! { ("narod.ru", &*P_YANDEX_RU), ("ziggo.nl", &*P_ZIGGO_NL), ].iter().copied().collect(); + + pub static ref PROVIDER_UPDATED: chrono::NaiveDate = chrono::NaiveDate::from_ymd(2020, 9, 19); } diff --git a/src/provider/mod.rs b/src/provider/mod.rs index c217813bb..bbc1e39cc 100644 --- a/src/provider/mod.rs +++ b/src/provider/mod.rs @@ -4,7 +4,8 @@ mod data; use crate::config::Config; use crate::dc_tools::EmailAddress; -use crate::provider::data::PROVIDER_DATA; +use crate::provider::data::{PROVIDER_DATA, PROVIDER_UPDATED}; +use chrono::{NaiveDateTime, NaiveTime}; #[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)] #[repr(u8)] @@ -91,11 +92,18 @@ pub fn get_provider_info(addr: &str) -> Option<&Provider> { None } +// returns update timestamp in seconds, UTC, compatible for comparison with time() and database times +pub fn get_provider_update_timestamp() -> i64 { + NaiveDateTime::new(*PROVIDER_UPDATED, NaiveTime::from_hms(0, 0, 0)).timestamp_millis() / 1_000 +} + #[cfg(test)] mod tests { #![allow(clippy::indexing_slicing)] use super::*; + use crate::dc_tools::time; + use chrono::NaiveDate; #[test] fn test_get_provider_info_unexistant() { @@ -138,4 +146,16 @@ mod tests { let provider = get_provider_info("user@googlemail.com").unwrap(); assert!(provider.status == Status::PREPARATION); } + + #[test] + fn test_get_provider_update_timestamp() { + let timestamp_past = NaiveDateTime::new( + NaiveDate::from_ymd(2020, 9, 9), + NaiveTime::from_hms(0, 0, 0), + ) + .timestamp_millis() + / 1_000; + assert!(get_provider_update_timestamp() <= time()); + assert!(get_provider_update_timestamp() > timestamp_past); + } } diff --git a/src/provider/update.py b/src/provider/update.py index 5ad15f1b2..d9bdd8d26 100755 --- a/src/provider/update.py +++ b/src/provider/update.py @@ -4,6 +4,7 @@ import sys import os import yaml +import datetime out_all = "" out_domains = "" @@ -169,6 +170,12 @@ if __name__ == "__main__": out_all += " pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [\n" out_all += out_domains; - out_all += " ].iter().copied().collect();\n}" + out_all += " ].iter().copied().collect();\n\n" + + now = datetime.datetime.utcnow() + out_all += " pub static ref PROVIDER_UPDATED: chrono::NaiveDate = "\ + "chrono::NaiveDate::from_ymd("+str(now.year)+", "+str(now.month)+", "+str(now.day)+");\n" + + out_all += "}" print(out_all) diff --git a/src/scheduler.rs b/src/scheduler.rs index 9688bbba1..6f2ea5ac5 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -3,6 +3,7 @@ use async_std::sync::{channel, Receiver, Sender}; use async_std::task; use crate::context::Context; +use crate::dc_tools::maybe_add_time_based_warnings; use crate::imap::Imap; use crate::job::{self, Thread}; use crate::{config::Config, message::MsgId, smtp::Smtp}; @@ -81,6 +82,8 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne warn!(ctx, "failed to close folder: {:?}", err); } + maybe_add_time_based_warnings(&ctx).await; + info = if ctx.get_config_bool(Config::InboxWatch).await { fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await } else { diff --git a/src/stock.rs b/src/stock.rs index 226a0946c..9c4366c77 100644 --- a/src/stock.rs +++ b/src/stock.rs @@ -219,6 +219,18 @@ pub enum StockMessage { #[strum(props(fallback = "Configuration failed. Error: “%1$s”"))] ConfigurationFailed = 84, + + #[strum(props( + fallback = "⚠️ Date or time of your device seem to be inaccurate (%1$s).\n\n\ + Adjust your clock ⏰🔧 to ensure your messages are received correctly." + ))] + BadTimeMsgBody = 85, + + #[strum(props(fallback = "⚠️ Your Delta Chat version might be outdated.\n\n\ + This may cause problems because your chat partners use newer versions - \ + and you are missing the latest features 😳\n\ + Please check https://get.delta.chat or your app store for updates."))] + UpdateReminderMsgBody = 86, } /*