Merge pull request #1901 from deltachat/validate-system-date

add device-message on bad system clock or on outdated app
This commit is contained in:
bjoern
2020-09-19 13:41:17 +02:00
committed by GitHub
9 changed files with 273 additions and 6 deletions

View File

@@ -4925,8 +4925,10 @@ void dc_event_unref(dc_event_t* event);
#define DC_STR_VIDEOCHAT_INVITATION 82 #define DC_STR_VIDEOCHAT_INVITATION 82
#define DC_STR_VIDEOCHAT_INVITE_MSG_BODY 83 #define DC_STR_VIDEOCHAT_INVITE_MSG_BODY 83
#define DC_STR_CONFIGURATION_FAILED 84 #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
/* /*
* @} * @}

View File

@@ -2756,14 +2756,35 @@ pub async fn add_device_msg_with_importance(
prepare_msg_blob(context, msg).await?; prepare_msg_blob(context, msg).await?;
chat_id.unarchive(context).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( context.sql.execute(
"INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,param,rfc724_mid) \ "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,timestamp_sent,timestamp_rcvd,type,state, txt,param,rfc724_mid) \
VALUES (?,?,?, ?,?,?, ?,?,?);", VALUES (?,?,?, ?,?,?,?,?, ?,?,?);",
paramsv![ paramsv![
chat_id, chat_id,
DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_DEVICE,
DC_CONTACT_ID_SELF, DC_CONTACT_ID_SELF,
dc_create_smeared_timestamp(context).await, timestamp_sort,
timestamp_sent,
timestamp_sent, // timestamp_sent equals timestamp_rcvd
msg.viewtype, msg.viewtype,
MessageState::InFresh, MessageState::InFresh,
msg.text.as_ref().cloned().unwrap_or_default(), msg.text.as_ref().cloned().unwrap_or_default(),

View File

@@ -108,6 +108,12 @@ pub const DC_GCL_ADD_SELF: usize = 0x02;
// unchanged user avatars are resent to the recipients every some days // unchanged user avatars are resent to the recipients every some days
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14; 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 /// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
pub const DC_CHAT_ID_DEADDROP: u32 = 1; 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) /// 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)

View File

@@ -15,9 +15,14 @@ use async_std::{fs, io};
use chrono::{Local, TimeZone}; use chrono::{Local, TimeZone};
use rand::{thread_rng, Rng}; 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::context::Context;
use crate::error::{bail, Error}; use crate::error::{bail, Error};
use crate::events::EventType; 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 /// Shortens a string to a specified length and adds "[...]" to the
/// end of the shortened string. /// end of the shortened string.
@@ -151,6 +156,73 @@ pub(crate) async fn dc_create_smeared_timestamps(context: &Context, count: usize
start 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 */ /* Message-ID tools */
pub(crate) fn dc_create_id() -> String { pub(crate) fn dc_create_id() -> String {
/* generate an id. the generated ID should be as short and as unique as possible: /* 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::<EmailAddress>().is_ok(), false); assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
} }
use crate::chat;
use crate::chatlist::Chatlist;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use proptest::prelude::*; use proptest::prelude::*;
proptest! { proptest! {
@@ -993,4 +1068,123 @@ mod tests {
assert_eq!(improve_single_line_input("Hi\naiae "), "Hi aiae"); assert_eq!(improve_single_line_input("Hi\naiae "), "Hi aiae");
assert_eq!(improve_single_line_input("\r\nahte\n\r"), "ahte"); 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);
}
} }

View File

@@ -807,4 +807,6 @@ lazy_static::lazy_static! {
("narod.ru", &*P_YANDEX_RU), ("narod.ru", &*P_YANDEX_RU),
("ziggo.nl", &*P_ZIGGO_NL), ("ziggo.nl", &*P_ZIGGO_NL),
].iter().copied().collect(); ].iter().copied().collect();
pub static ref PROVIDER_UPDATED: chrono::NaiveDate = chrono::NaiveDate::from_ymd(2020, 9, 19);
} }

View File

@@ -4,7 +4,8 @@ mod data;
use crate::config::Config; use crate::config::Config;
use crate::dc_tools::EmailAddress; 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)] #[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)]
#[repr(u8)] #[repr(u8)]
@@ -91,11 +92,18 @@ pub fn get_provider_info(addr: &str) -> Option<&Provider> {
None 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)] #[cfg(test)]
mod tests { mod tests {
#![allow(clippy::indexing_slicing)] #![allow(clippy::indexing_slicing)]
use super::*; use super::*;
use crate::dc_tools::time;
use chrono::NaiveDate;
#[test] #[test]
fn test_get_provider_info_unexistant() { fn test_get_provider_info_unexistant() {
@@ -138,4 +146,16 @@ mod tests {
let provider = get_provider_info("user@googlemail.com").unwrap(); let provider = get_provider_info("user@googlemail.com").unwrap();
assert!(provider.status == Status::PREPARATION); 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);
}
} }

View File

@@ -4,6 +4,7 @@
import sys import sys
import os import os
import yaml import yaml
import datetime
out_all = "" out_all = ""
out_domains = "" out_domains = ""
@@ -169,6 +170,12 @@ if __name__ == "__main__":
out_all += " pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [\n" out_all += " pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [\n"
out_all += out_domains; 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) print(out_all)

View File

@@ -3,6 +3,7 @@ use async_std::sync::{channel, Receiver, Sender};
use async_std::task; use async_std::task;
use crate::context::Context; use crate::context::Context;
use crate::dc_tools::maybe_add_time_based_warnings;
use crate::imap::Imap; use crate::imap::Imap;
use crate::job::{self, Thread}; use crate::job::{self, Thread};
use crate::{config::Config, message::MsgId, smtp::Smtp}; 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); warn!(ctx, "failed to close folder: {:?}", err);
} }
maybe_add_time_based_warnings(&ctx).await;
info = if ctx.get_config_bool(Config::InboxWatch).await { info = if ctx.get_config_bool(Config::InboxWatch).await {
fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await
} else { } else {

View File

@@ -219,6 +219,18 @@ pub enum StockMessage {
#[strum(props(fallback = "Configuration failed. Error: “%1$s”"))] #[strum(props(fallback = "Configuration failed. Error: “%1$s”"))]
ConfigurationFailed = 84, 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,
} }
/* /*