From ec9d104cf3ae4964b525e023b5c3d3c4e506d751 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 7 Feb 2024 20:23:11 +0100 Subject: [PATCH] Basic self-reporting, core part (#5129) Part of https://github.com/deltachat/deltachat-android/issues/2909 For now, this is only sending a few basic metrics. --- deltachat-jsonrpc/src/api.rs | 5 ++ src/config.rs | 4 ++ src/context.rs | 124 +++++++++++++++++++++++++++++++++-- src/peerstate.rs | 27 ++++++-- 4 files changed, 148 insertions(+), 12 deletions(-) diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index e2bf6c62f9..fa0f6bed70 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -325,6 +325,11 @@ impl CommandApi { ctx.get_info().await } + async fn draft_self_report(&self, account_id: u32) -> Result { + let ctx = self.get_context(account_id).await?; + Ok(ctx.draft_self_report().await?.to_u32()) + } + /// Sets the given configuration key. async fn set_config(&self, account_id: u32, key: String, value: Option) -> Result<()> { let ctx = self.get_context(account_id).await?; diff --git a/src/config.rs b/src/config.rs index 0594939e38..5b26a3193f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -347,6 +347,10 @@ pub enum Config { /// Row ID of the key in the `keypairs` table /// used for signatures, encryption to self and included in `Autocrypt` header. KeyId, + + /// This key is sent to the self_reporting bot so that the bot can recognize the user + /// without storing the email address + SelfReportingId, } impl Config { diff --git a/src/context.rs b/src/context.rs index 38bf11e0d8..a52097ab97 100644 --- a/src/context.rs +++ b/src/context.rs @@ -10,25 +10,30 @@ use std::time::{Duration, Instant, SystemTime}; use anyhow::{bail, ensure, Context as _, Result}; use async_channel::{self as channel, Receiver, Sender}; +use pgp::SignedPublicKey; use ratelimit::Ratelimit; use tokio::sync::{Mutex, Notify, RwLock}; -use crate::chat::{get_chat_cnt, ChatId}; +use crate::aheader::EncryptPreference; +use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus}; use crate::config::Config; -use crate::constants::{DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSION_STR}; +use crate::constants::{ + DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR, +}; use crate::contact::Contact; use crate::debug_logging::DebugLogging; use crate::events::{Event, EventEmitter, EventType, Events}; use crate::imap::{FolderMeaning, Imap, ServerMetadata}; -use crate::key::{load_self_public_key, DcKey as _}; +use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _}; use crate::login_param::LoginParam; -use crate::message::{self, MessageState, MsgId}; +use crate::message::{self, Message, MessageState, MsgId, Viewtype}; +use crate::peerstate::Peerstate; use crate::quota::QuotaInfo; use crate::scheduler::{convert_folder_meaning, SchedulerState}; use crate::sql::Sql; use crate::stock_str::StockStrings; use crate::timesmearing::SmearedTimestamp; -use crate::tools::{duration_to_str, time}; +use crate::tools::{create_id, duration_to_str, time}; /// Builder for the [`Context`]. /// @@ -859,6 +864,91 @@ impl Context { Ok(res) } + async fn get_self_report(&self) -> Result { + let mut res = String::new(); + res += &format!("core_version {}\n", get_version_str()); + + let num_msgs: u32 = self + .sql + .query_get_value( + "SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id!=?", + (DC_CHAT_ID_TRASH,), + ) + .await? + .unwrap_or_default(); + res += &format!("num_msgs {}\n", num_msgs); + + let num_chats: u32 = self + .sql + .query_get_value("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked!=1", ()) + .await? + .unwrap_or_default(); + res += &format!("num_chats {}\n", num_chats); + + let db_size = tokio::fs::metadata(&self.sql.dbfile).await?.len(); + res += &format!("db_size_bytes {}\n", db_size); + + let secret_key = &load_self_secret_key(self).await?.primary_key; + let key_created = secret_key.created_at().timestamp(); + res += &format!("key_created {}\n", key_created); + + let self_reporting_id = match self.get_config(Config::SelfReportingId).await? { + Some(id) => id, + None => { + let id = create_id(); + self.set_config(Config::SelfReportingId, Some(&id)).await?; + id + } + }; + res += &format!("self_reporting_id {}", self_reporting_id); + + Ok(res) + } + + /// Drafts a message with statistics about the usage of Delta Chat. + /// The user can inspect the message if they want, and then hit "Send". + /// + /// On the other end, a bot will receive the message and make it available + /// to Delta Chat's developers. + pub async fn draft_self_report(&self) -> Result { + const SELF_REPORTING_BOT: &str = "self_reporting@testrun.org"; + + let contact_id = Contact::create(self, "Statistics bot", SELF_REPORTING_BOT).await?; + let chat_id = ChatId::create_for_contact(self, contact_id).await?; + + // We're including the bot's public key in Delta Chat + // so that the first message to the bot can directly be encrypted: + let public_key = SignedPublicKey::from_base64( + "xjMEZbfBlBYJKwYBBAHaRw8BAQdABpLWS2PUIGGo4pslVt4R8sylP5wZihmhf1DTDr3oCM\ + PNHDxzZWxmX3JlcG9ydGluZ0B0ZXN0cnVuLm9yZz7CiwQQFggAMwIZAQUCZbfBlAIbAwQLCQgHBhUI\ + CQoLAgMWAgEWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohD8dAQCQV7CoH6UP4PD+Nq\ + I4kW5tbbqdh2AnDROg60qotmLExAEAxDfd3QHAK9f8b9qQUbLmHIztCLxhEuVbWPBEYeVW0gvOOARl\ + t8GUEgorBgEEAZdVAQUBAQdAMBUhYoAAcI625vGZqnM5maPX4sGJ7qvJxPAFILPy6AcDAQgHwngEGB\ + YIACAFAmW3wZQCGwwWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohPwCAQCvzk1ObIkj\ + 2GqsuIfaULlgdnfdZY8LNary425CEfHZDQD5AblXVrlMO1frdlc/Vo9z3pEeCrfYdD7ITD3/OeVoiQ\ + 4=", + )?; + let mut peerstate = Peerstate::from_public_key( + SELF_REPORTING_BOT, + 0, + EncryptPreference::Mutual, + &public_key, + ); + let fingerprint = public_key.fingerprint(); + peerstate.set_verified(public_key, fingerprint, "".to_string())?; + peerstate.save_to_db(&self.sql).await?; + chat_id + .set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id)) + .await?; + + let mut msg = Message::new(Viewtype::Text); + msg.text = self.get_self_report().await?; + + chat_id.set_draft(self, Some(&mut msg)).await?; + + Ok(chat_id) + } + /// Get a list of fresh, unmuted messages in unblocked chats. /// /// The list starts with the most recent message @@ -1129,8 +1219,9 @@ mod tests { use crate::constants::Chattype; use crate::contact::ContactId; use crate::message::{Message, Viewtype}; + use crate::mimeparser::SystemMessage; use crate::receive_imf::receive_imf; - use crate::test_utils::TestContext; + use crate::test_utils::{get_chat_msg, TestContext}; use crate::tools::create_outgoing_rfc724_mid; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -1369,6 +1460,7 @@ mod tests { "mail_security", "notify_about_wrong_pw", "save_mime_headers", + "self_reporting_id", "selfstatus", "send_server", "send_user", @@ -1669,4 +1761,24 @@ mod tests { Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_draft_self_report() -> Result<()> { + let alice = TestContext::new_alice().await; + + let chat_id = alice.draft_self_report().await?; + let msg = get_chat_msg(&alice, chat_id, 0, 1).await; + assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled); + + let chat = Chat::load_from_db(&alice, chat_id).await?; + assert!(chat.is_protected()); + + let mut draft = chat_id.get_draft(&alice).await?.unwrap(); + assert!(draft.text.starts_with("core_version")); + + // Test that sending into the protected chat works: + let _sent = alice.send_msg(chat_id, &mut draft).await; + + Ok(()) + } } diff --git a/src/peerstate.rs b/src/peerstate.rs index a8b9909474..fc3f39e15b 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -97,13 +97,28 @@ pub struct Peerstate { impl Peerstate { /// Creates a peerstate from the `Autocrypt` header. pub fn from_header(header: &Aheader, message_time: i64) -> Self { + Self::from_public_key( + &header.addr, + message_time, + header.prefer_encrypt, + &header.public_key, + ) + } + + /// Creates a peerstate from the given public key. + pub fn from_public_key( + addr: &str, + last_seen: i64, + prefer_encrypt: EncryptPreference, + public_key: &SignedPublicKey, + ) -> Self { Peerstate { - addr: header.addr.clone(), - last_seen: message_time, - last_seen_autocrypt: message_time, - prefer_encrypt: header.prefer_encrypt, - public_key: Some(header.public_key.clone()), - public_key_fingerprint: Some(header.public_key.fingerprint()), + addr: addr.to_string(), + last_seen, + last_seen_autocrypt: last_seen, + prefer_encrypt, + public_key: Some(public_key.clone()), + public_key_fingerprint: Some(public_key.fingerprint()), gossip_key: None, gossip_key_fingerprint: None, gossip_timestamp: 0,