Skip to content

Commit b1c57ee

Browse files
committed
feat: Opt-in weekly sending of statistics
This way, the statistics / self-reporting bot will be made into an opt-in regular sending of statistics, where you enable the setting once and then they will be sent automatically. The statistics will be sent to a bot, so that the user can see exactly which data is being sent, and how often. The chat will be archived and muted by default, so that it doesn't disturb the user.
1 parent 7e4822c commit b1c57ee

File tree

15 files changed

+1351
-193
lines changed

15 files changed

+1351
-193
lines changed
File renamed without changes.

deltachat-jsonrpc/src/api.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -383,11 +383,6 @@ impl CommandApi {
383383
Ok(BlobObject::create_and_deduplicate(&ctx, file, file)?.to_abs_path())
384384
}
385385

386-
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
387-
let ctx = self.get_context(account_id).await?;
388-
Ok(ctx.draft_self_report().await?.to_u32())
389-
}
390-
391386
/// Sets the given configuration key.
392387
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
393388
let ctx = self.get_context(account_id).await?;
@@ -877,12 +872,22 @@ impl CommandApi {
877872
/// **qr**: The text of the scanned QR code. Typically, the same string as given
878873
/// to `check_qr()`.
879874
///
875+
/// **source** and **uipath** are for statistics-sending,
876+
/// if the user enabled it in the settings;
877+
/// if you don't have statistics-sending implemented, just pass `None` here.
878+
///
880879
/// **returns**: The chat ID of the joined chat, the UI may redirect to the this chat.
881880
/// A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
882881
///
883-
async fn secure_join(&self, account_id: u32, qr: String) -> Result<u32> {
882+
async fn secure_join(
883+
&self,
884+
account_id: u32,
885+
qr: String,
886+
source: Option<u32>,
887+
uipath: Option<u32>,
888+
) -> Result<u32> {
884889
let ctx = self.get_context(account_id).await?;
885-
let chat_id = securejoin::join_securejoin(&ctx, &qr).await?;
890+
let chat_id = securejoin::join_securejoin_with_source(&ctx, &qr, source, uipath).await?;
886891
Ok(chat_id.to_u32())
887892
}
888893

deltachat-jsonrpc/src/api/types/chat.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::time::{Duration, SystemTime};
22

3-
use anyhow::{bail, Context as _, Result};
4-
use deltachat::chat::{self, get_chat_contacts, get_past_chat_contacts, ChatVisibility};
3+
use anyhow::{Context as _, Result, bail};
4+
use deltachat::chat::{self, ChatVisibility, get_chat_contacts, get_past_chat_contacts};
55
use deltachat::chat::{Chat, ChatId};
66
use deltachat::constants::Chattype;
77
use deltachat::contact::{Contact, ContactId};

deltachat-jsonrpc/src/api/types/chat_list.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use deltachat::chatlist::get_last_message_for_chat;
44
use deltachat::constants::*;
55
use deltachat::contact::{Contact, ContactId};
66
use deltachat::{
7-
chat::{get_chat_contacts, ChatVisibility},
7+
chat::{ChatVisibility, get_chat_contacts},
88
chatlist::Chatlist,
99
};
1010
use num_traits::cast::ToPrimitive;

deltachat-jsonrpc/src/api/types/http.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub struct HttpResponse {
1616

1717
impl From<CoreHttpResponse> for HttpResponse {
1818
fn from(response: CoreHttpResponse) -> Self {
19-
use base64::{engine::general_purpose, Engine as _};
19+
use base64::{Engine as _, engine::general_purpose};
2020
let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob);
2121
let mimetype = response.mimetype;
2222
let encoding = response.encoding;

src/config.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,9 +431,26 @@ pub enum Config {
431431
/// used for signatures, encryption to self and included in `Autocrypt` header.
432432
KeyId,
433433

434-
/// This key is sent to the self_reporting bot so that the bot can recognize the user
434+
/// Send statistics to Delta Chat's developers.
435+
/// Can be exposed to the user as a setting.
436+
SendStatistics,
437+
438+
/// Last time statistics were sent to Delta Chat's developers
439+
LastStatisticsSent,
440+
441+
/// This key is sent to the statistics bot so that the bot can recognize the user
435442
/// without storing the email address
436-
SelfReportingId,
443+
StatisticsId,
444+
445+
/// The last message id that was already included in the previously sent statistics,
446+
/// or that already existed before the user opted in.
447+
/// Only messages with an id larger than this
448+
/// will be counted in the next statistics.
449+
StatsLastExcludedMsgId,
450+
451+
/// The last contact id that already existed when statistics-sending was enabled.
452+
/// All newer contacts get the `"new": true` attribute.
453+
StatsLastOldContactId,
437454

438455
/// MsgId of webxdc map integration.
439456
WebxdcIntegration,
@@ -827,6 +844,11 @@ impl Context {
827844
.await?;
828845
}
829846
}
847+
Config::SendStatistics => {
848+
self.sql.set_raw_config(key.as_ref(), value).await?;
849+
crate::statistics::set_last_excluded_msg_id(self).await?;
850+
crate::statistics::set_last_old_contact_id(self).await?;
851+
}
830852
_ => {
831853
self.sql.set_raw_config(key.as_ref(), value).await?;
832854
}

src/context.rs

Lines changed: 24 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,30 @@ use std::time::Duration;
1010

1111
use anyhow::{Context as _, Result, bail, ensure};
1212
use async_channel::{self as channel, Receiver, Sender};
13-
use pgp::types::PublicKeyTrait;
1413
use ratelimit::Ratelimit;
1514
use tokio::sync::{Mutex, Notify, RwLock};
1615

17-
use crate::chat::{ChatId, ProtectionStatus, get_chat_cnt};
16+
use crate::chat::{ChatId, get_chat_cnt};
1817
use crate::chatlist_events;
1918
use crate::config::Config;
20-
use crate::constants::{
21-
self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR,
22-
};
23-
use crate::contact::{Contact, ContactId, import_vcard, mark_contact_id_as_verified};
19+
use crate::constants::{self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSION_STR};
20+
use crate::contact::{Contact, ContactId};
2421
use crate::debug_logging::DebugLogging;
25-
use crate::download::DownloadState;
2622
use crate::events::{Event, EventEmitter, EventType, Events};
2723
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
28-
use crate::key::{load_self_secret_key, self_fingerprint};
24+
use crate::key::self_fingerprint;
2925
use crate::log::{info, warn};
3026
use crate::logged_debug_assert;
3127
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
32-
use crate::message::{self, Message, MessageState, MsgId};
33-
use crate::param::{Param, Params};
28+
use crate::message::{self, MessageState, MsgId};
3429
use crate::peer_channels::Iroh;
3530
use crate::push::PushSubscriber;
3631
use crate::quota::QuotaInfo;
3732
use crate::scheduler::{SchedulerState, convert_folder_meaning};
3833
use crate::sql::Sql;
3934
use crate::stock_str::StockStrings;
4035
use crate::timesmearing::SmearedTimestamp;
41-
use crate::tools::{self, create_id, duration_to_str, time, time_elapsed};
36+
use crate::tools::{self, duration_to_str, time, time_elapsed};
4237

4338
/// Builder for the [`Context`].
4439
///
@@ -1076,154 +1071,31 @@ impl Context {
10761071
.await?
10771072
.unwrap_or_default(),
10781073
);
1074+
res.insert(
1075+
"statistics_id",
1076+
self.get_config_bool(Config::StatisticsId)
1077+
.await?
1078+
.to_string(),
1079+
);
1080+
res.insert(
1081+
"send_statistics",
1082+
self.get_config_bool(Config::SendStatistics)
1083+
.await?
1084+
.to_string(),
1085+
);
1086+
res.insert(
1087+
"last_statistics_sent",
1088+
self.get_config_i64(Config::LastStatisticsSent)
1089+
.await?
1090+
.to_string(),
1091+
);
10791092

10801093
let elapsed = time_elapsed(&self.creation_time);
10811094
res.insert("uptime", duration_to_str(elapsed));
10821095

10831096
Ok(res)
10841097
}
10851098

1086-
async fn get_self_report(&self) -> Result<String> {
1087-
#[derive(Default)]
1088-
struct ChatNumbers {
1089-
protected: u32,
1090-
opportunistic_dc: u32,
1091-
opportunistic_mua: u32,
1092-
unencrypted_dc: u32,
1093-
unencrypted_mua: u32,
1094-
}
1095-
1096-
let mut res = String::new();
1097-
res += &format!("core_version {}\n", get_version_str());
1098-
1099-
let num_msgs: u32 = self
1100-
.sql
1101-
.query_get_value(
1102-
"SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id!=?",
1103-
(DC_CHAT_ID_TRASH,),
1104-
)
1105-
.await?
1106-
.unwrap_or_default();
1107-
res += &format!("num_msgs {num_msgs}\n");
1108-
1109-
let num_chats: u32 = self
1110-
.sql
1111-
.query_get_value("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked!=1", ())
1112-
.await?
1113-
.unwrap_or_default();
1114-
res += &format!("num_chats {num_chats}\n");
1115-
1116-
let db_size = tokio::fs::metadata(&self.sql.dbfile).await?.len();
1117-
res += &format!("db_size_bytes {db_size}\n");
1118-
1119-
let secret_key = &load_self_secret_key(self).await?.primary_key;
1120-
let key_created = secret_key.public_key().created_at().timestamp();
1121-
res += &format!("key_created {key_created}\n");
1122-
1123-
// how many of the chats active in the last months are:
1124-
// - protected
1125-
// - opportunistic-encrypted and the contact uses Delta Chat
1126-
// - opportunistic-encrypted and the contact uses a classical MUA
1127-
// - unencrypted and the contact uses Delta Chat
1128-
// - unencrypted and the contact uses a classical MUA
1129-
let three_months_ago = time().saturating_sub(3600 * 24 * 30 * 3);
1130-
let chats = self
1131-
.sql
1132-
.query_map(
1133-
"SELECT c.protected, m.param, m.msgrmsg
1134-
FROM chats c
1135-
JOIN msgs m
1136-
ON c.id=m.chat_id
1137-
AND m.id=(
1138-
SELECT id
1139-
FROM msgs
1140-
WHERE chat_id=c.id
1141-
AND hidden=0
1142-
AND download_state=?
1143-
AND to_id!=?
1144-
ORDER BY timestamp DESC, id DESC LIMIT 1)
1145-
WHERE c.id>9
1146-
AND (c.blocked=0 OR c.blocked=2)
1147-
AND IFNULL(m.timestamp,c.created_timestamp) > ?
1148-
GROUP BY c.id",
1149-
(DownloadState::Done, ContactId::INFO, three_months_ago),
1150-
|row| {
1151-
let protected: ProtectionStatus = row.get(0)?;
1152-
let message_param: Params =
1153-
row.get::<_, String>(1)?.parse().unwrap_or_default();
1154-
let is_dc_message: bool = row.get(2)?;
1155-
Ok((protected, message_param, is_dc_message))
1156-
},
1157-
|rows| {
1158-
let mut chats = ChatNumbers::default();
1159-
for row in rows {
1160-
let (protected, message_param, is_dc_message) = row?;
1161-
let encrypted = message_param
1162-
.get_bool(Param::GuaranteeE2ee)
1163-
.unwrap_or(false);
1164-
1165-
if protected == ProtectionStatus::Protected {
1166-
chats.protected += 1;
1167-
} else if encrypted {
1168-
if is_dc_message {
1169-
chats.opportunistic_dc += 1;
1170-
} else {
1171-
chats.opportunistic_mua += 1;
1172-
}
1173-
} else if is_dc_message {
1174-
chats.unencrypted_dc += 1;
1175-
} else {
1176-
chats.unencrypted_mua += 1;
1177-
}
1178-
}
1179-
Ok(chats)
1180-
},
1181-
)
1182-
.await?;
1183-
res += &format!("chats_protected {}\n", chats.protected);
1184-
res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc);
1185-
res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua);
1186-
res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc);
1187-
res += &format!("chats_unencrypted_mua {}\n", chats.unencrypted_mua);
1188-
1189-
let self_reporting_id = match self.get_config(Config::SelfReportingId).await? {
1190-
Some(id) => id,
1191-
None => {
1192-
let id = create_id();
1193-
self.set_config(Config::SelfReportingId, Some(&id)).await?;
1194-
id
1195-
}
1196-
};
1197-
res += &format!("self_reporting_id {self_reporting_id}");
1198-
1199-
Ok(res)
1200-
}
1201-
1202-
/// Drafts a message with statistics about the usage of Delta Chat.
1203-
/// The user can inspect the message if they want, and then hit "Send".
1204-
///
1205-
/// On the other end, a bot will receive the message and make it available
1206-
/// to Delta Chat's developers.
1207-
pub async fn draft_self_report(&self) -> Result<ChatId> {
1208-
const SELF_REPORTING_BOT_VCARD: &str = include_str!("../assets/self-reporting-bot.vcf");
1209-
let contact_id: ContactId = *import_vcard(self, SELF_REPORTING_BOT_VCARD)
1210-
.await?
1211-
.first()
1212-
.context("Self reporting bot vCard does not contain a contact")?;
1213-
mark_contact_id_as_verified(self, contact_id, ContactId::SELF).await?;
1214-
1215-
let chat_id = ChatId::create_for_contact(self, contact_id).await?;
1216-
chat_id
1217-
.set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id))
1218-
.await?;
1219-
1220-
let mut msg = Message::new_text(self.get_self_report().await?);
1221-
1222-
chat_id.set_draft(self, Some(&mut msg)).await?;
1223-
1224-
Ok(chat_id)
1225-
}
1226-
12271099
/// Get a list of fresh, unmuted messages in unblocked chats.
12281100
///
12291101
/// The list starts with the most recent message

src/context/context_tests.rs

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ use super::*;
66
use crate::chat::{Chat, MuteDuration, get_chat_contacts, get_chat_msgs, send_msg, set_muted};
77
use crate::chatlist::Chatlist;
88
use crate::constants::Chattype;
9-
use crate::mimeparser::SystemMessage;
9+
use crate::message::Message;
1010
use crate::receive_imf::receive_imf;
11-
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, get_chat_msg};
11+
use crate::test_utils::{E2EE_INFO_MSGS, TestContext};
1212
use crate::tools::{SystemTime, create_outgoing_rfc724_mid};
1313

1414
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -276,7 +276,6 @@ async fn test_get_info_completeness() {
276276
"mail_port",
277277
"mail_security",
278278
"notify_about_wrong_pw",
279-
"self_reporting_id",
280279
"selfstatus",
281280
"send_server",
282281
"send_user",
@@ -296,6 +295,8 @@ async fn test_get_info_completeness() {
296295
"webxdc_integration",
297296
"device_token",
298297
"encrypted_device_token",
298+
"stats_last_excluded_msg_id",
299+
"stats_last_old_contact_id",
299300
];
300301
let t = TestContext::new().await;
301302
let info = t.get_info().await.unwrap();
@@ -598,26 +599,6 @@ async fn test_get_next_msgs() -> Result<()> {
598599
Ok(())
599600
}
600601

601-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
602-
async fn test_draft_self_report() -> Result<()> {
603-
let alice = TestContext::new_alice().await;
604-
605-
let chat_id = alice.draft_self_report().await?;
606-
let msg = get_chat_msg(&alice, chat_id, 0, 1).await;
607-
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
608-
609-
let chat = Chat::load_from_db(&alice, chat_id).await?;
610-
assert!(chat.is_protected());
611-
612-
let mut draft = chat_id.get_draft(&alice).await?.unwrap();
613-
assert!(draft.text.starts_with("core_version"));
614-
615-
// Test that sending into the protected chat works:
616-
let _sent = alice.send_msg(chat_id, &mut draft).await;
617-
618-
Ok(())
619-
}
620-
621602
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
622603
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
623604
let alice = TestContext::new_alice().await;

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ pub mod html;
9898
pub mod net;
9999
pub mod plaintext;
100100
mod push;
101+
pub mod statistics;
101102
pub mod summary;
102103

103104
mod debug_logging;

0 commit comments

Comments
 (0)