1use std::collections::{BTreeMap, BTreeSet};
7
8use anyhow::{Context as _, Result};
9use deltachat_derive::FromSql;
10use num_traits::ToPrimitive;
11use pgp::types::PublicKeyTrait;
12use rand::distr::SampleString as _;
13use rusqlite::OptionalExtension;
14use serde::Serialize;
15
16use crate::chat::{self, ChatId, MuteDuration};
17use crate::config::Config;
18use crate::constants::{Chattype, DC_VERSION_STR};
19use crate::contact::{Contact, ContactId, Origin, import_vcard, mark_contact_id_as_verified};
20use crate::context::Context;
21use crate::key::load_self_public_keyring;
22use crate::log::LogExt;
23use crate::message::{Message, Viewtype};
24use crate::securejoin::QrInvite;
25use crate::tools::time;
26
27pub(crate) const STATISTICS_BOT_EMAIL: &str = "self_reporting@testrun.org";
28const STATISTICS_BOT_VCARD: &str = include_str!("../assets/statistics-bot.vcf");
29const SENDING_INTERVAL_SECONDS: i64 = 3600 * 24 * 7; const MESSAGE_STATS_UPDATE_INTERVAL_SECONDS: i64 = 4 * 60; #[derive(Serialize)]
34struct Statistics {
35 core_version: String,
36 key_create_timestamps: Vec<i64>,
37 stats_id: String,
38 is_chatmail: bool,
39 contact_stats: Vec<ContactStat>,
40 message_stats: BTreeMap<Chattype, MessageStats>,
41 securejoin_sources: SecurejoinSources,
42 securejoin_uipaths: SecurejoinUiPaths,
43 securejoin_invites: Vec<JoinedInvite>,
44 sending_enabled_timestamps: Vec<i64>,
45 sending_disabled_timestamps: Vec<i64>,
46}
47
48#[derive(Serialize, PartialEq)]
49enum VerifiedStatus {
50 Direct,
51 Transitive,
52 TransitiveViaBot,
53 Opportunistic,
54 Unencrypted,
55}
56
57#[derive(Serialize)]
58struct ContactStat {
59 #[serde(skip_serializing)]
60 id: ContactId,
61
62 verified: VerifiedStatus,
63
64 #[serde(skip_serializing_if = "is_false")]
68 bot: bool,
69
70 #[serde(skip_serializing_if = "is_false")]
71 direct_chat: bool,
72
73 last_seen: u64,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
76 transitive_chain: Option<u32>,
77
78 #[serde(skip_serializing_if = "is_false")]
80 new: bool,
81}
82
83fn is_false(b: &bool) -> bool {
84 !b
85}
86
87#[derive(Serialize, Default)]
88struct MessageStats {
89 verified: u32,
90 unverified_encrypted: u32,
91 unencrypted: u32,
92 only_to_self: u32,
93}
94
95#[repr(u32)]
98#[derive(
99 Debug, Clone, Copy, ToPrimitive, FromPrimitive, FromSql, PartialEq, Eq, PartialOrd, Ord,
100)]
101pub enum SecurejoinSource {
102 Unknown = 0,
104 ExternalLink = 1,
106 InternalLink = 2,
108 Clipboard = 3,
110 ImageLoaded = 4,
112 Scan = 5,
114}
115
116#[derive(Serialize)]
117struct SecurejoinSources {
118 unknown: u32,
119 external_link: u32,
120 internal_link: u32,
121 clipboard: u32,
122 image_loaded: u32,
123 scan: u32,
124}
125
126#[derive(
129 Debug, Clone, Copy, ToPrimitive, FromPrimitive, FromSql, PartialEq, Eq, PartialOrd, Ord,
130)]
131pub enum SecurejoinUiPath {
132 Unknown = 0,
134 QrIcon = 1,
136 NewContact = 2,
139}
140
141#[derive(Serialize)]
142struct SecurejoinUiPaths {
143 other: u32,
144 qr_icon: u32,
145 new_contact: u32,
146}
147
148#[derive(Serialize)]
151struct JoinedInvite {
152 already_existed: bool,
155 already_verified: bool,
158 typ: String,
164}
165
166pub(crate) async fn pre_sending_config_change(
167 context: &Context,
168 old_value: bool,
169 new_value: bool,
170) -> Result<()> {
171 ensure_last_old_contact_id(context).await?;
174 stats_id(context).await?;
177
178 if old_value != new_value {
179 if new_value {
180 set_last_counted_msg_id(context).await?;
182 } else {
183 update_message_stats(context).await?;
185 }
186
187 let sql_table = if new_value {
188 "stats_sending_enabled_events"
189 } else {
190 "stats_sending_disabled_events"
191 };
192
193 context
194 .sql
195 .execute(&format!("INSERT INTO {sql_table} VALUES(?)"), (time(),))
196 .await?;
197 }
198
199 Ok(())
200}
201
202pub async fn maybe_send_stats(context: &Context) -> Result<Option<ChatId>> {
209 if should_send_stats(context).await?
210 && time_has_passed(context, Config::StatsLastSent, SENDING_INTERVAL_SECONDS).await?
211 {
212 let chat_id = send_stats(context).await?;
213
214 return Ok(Some(chat_id));
215 }
216 Ok(None)
217}
218
219pub(crate) async fn maybe_update_message_stats(context: &Context) -> Result<()> {
220 if should_send_stats(context).await?
221 && time_has_passed(
222 context,
223 Config::StatsLastUpdate,
224 MESSAGE_STATS_UPDATE_INTERVAL_SECONDS,
225 )
226 .await?
227 {
228 update_message_stats(context).await?;
229 }
230
231 Ok(())
232}
233
234async fn time_has_passed(context: &Context, config: Config, seconds: i64) -> Result<bool> {
235 let last_time = context.get_config_i64(config).await?;
236 let next_time = last_time.saturating_add(seconds);
237
238 let res = if next_time <= time() {
239 context
242 .set_config_internal(config, Some(&time().to_string()))
243 .await?;
244 true
245 } else {
246 if time() < last_time {
247 context
251 .set_config_internal(config, Some(&time().to_string()))
252 .await?;
253 }
254 false
255 };
256
257 Ok(res)
258}
259
260#[allow(clippy::unused_async, unused)]
261pub(crate) async fn should_send_stats(context: &Context) -> Result<bool> {
262 #[cfg(any(target_os = "android", test))]
263 {
264 context.get_config_bool(Config::StatsSending).await
265 }
266
267 #[cfg(not(any(target_os = "android", test)))]
271 {
272 Ok(false)
273 }
274}
275
276async fn send_stats(context: &Context) -> Result<ChatId> {
277 info!(context, "Sending statistics.");
278
279 update_message_stats(context).await?;
280
281 let chat_id = get_stats_chat_id(context).await?;
282
283 let mut msg = Message::new(Viewtype::File);
284 msg.set_text(crate::stock_str::stats_msg_body(context).await);
285
286 let stats = get_stats(context).await?;
287
288 msg.set_file_from_bytes(
289 context,
290 "statistics.txt",
291 stats.as_bytes(),
292 Some("text/plain"),
293 )?;
294
295 chat::send_msg(context, chat_id, &mut msg)
296 .await
297 .context("Failed to send statistics message")
298 .log_err(context)
299 .ok();
300
301 Ok(chat_id)
302}
303
304async fn set_last_counted_msg_id(context: &Context) -> Result<()> {
305 context
306 .sql
307 .execute(
308 "UPDATE stats_msgs
309 SET last_counted_msg_id=(SELECT MAX(id) FROM msgs)",
310 (),
311 )
312 .await?;
313
314 Ok(())
315}
316
317async fn ensure_last_old_contact_id(context: &Context) -> Result<()> {
318 if context.config_exists(Config::StatsLastOldContactId).await? {
319 return Ok(());
322 }
323
324 let last_contact_id: u64 = context
325 .sql
326 .query_get_value("SELECT MAX(id) FROM contacts", ())
327 .await?
328 .unwrap_or(0);
329
330 context
331 .sql
332 .set_raw_config(
333 Config::StatsLastOldContactId.as_ref(),
334 Some(&last_contact_id.to_string()),
335 )
336 .await?;
337
338 Ok(())
339}
340
341async fn get_stats(context: &Context) -> Result<String> {
342 let last_old_contact = context
345 .get_config_u32(Config::StatsLastOldContactId)
346 .await?;
347
348 let key_create_timestamps: Vec<i64> = load_self_public_keyring(context)
349 .await?
350 .iter()
351 .map(|k| k.created_at().timestamp())
352 .collect();
353
354 let sending_enabled_timestamps =
355 get_timestamps(context, "stats_sending_enabled_events").await?;
356 let sending_disabled_timestamps =
357 get_timestamps(context, "stats_sending_disabled_events").await?;
358
359 let stats = Statistics {
360 core_version: DC_VERSION_STR.to_string(),
361 key_create_timestamps,
362 stats_id: stats_id(context).await?,
363 is_chatmail: context.is_chatmail().await?,
364 contact_stats: get_contact_stats(context, last_old_contact).await?,
365 message_stats: get_message_stats(context).await?,
366 securejoin_sources: get_securejoin_source_stats(context).await?,
367 securejoin_uipaths: get_securejoin_uipath_stats(context).await?,
368 securejoin_invites: get_securejoin_invite_stats(context).await?,
369 sending_enabled_timestamps,
370 sending_disabled_timestamps,
371 };
372
373 Ok(serde_json::to_string_pretty(&stats)?)
374}
375
376async fn get_timestamps(context: &Context, sql_table: &str) -> Result<Vec<i64>> {
377 context
378 .sql
379 .query_map_vec(
380 &format!("SELECT timestamp FROM {sql_table} LIMIT 1000"),
381 (),
382 |row| {
383 let timestamp: i64 = row.get(0)?;
384 Ok(timestamp)
385 },
386 )
387 .await
388}
389
390pub(crate) async fn stats_id(context: &Context) -> Result<String> {
391 Ok(match context.get_config(Config::StatsId).await? {
392 Some(id) => id,
393 None => {
394 let id = rand::distr::Alphabetic
395 .sample_string(&mut rand::rng(), 25)
396 .to_lowercase();
397 context
398 .set_config_internal(Config::StatsId, Some(&id))
399 .await?;
400 id
401 }
402 })
403}
404
405async fn get_stats_chat_id(context: &Context) -> Result<ChatId, anyhow::Error> {
406 let contact_id: ContactId = *import_vcard(context, STATISTICS_BOT_VCARD)
407 .await?
408 .first()
409 .context("Statistics bot vCard does not contain a contact")?;
410 mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
411
412 let chat_id = if let Some(res) = ChatId::lookup_by_contact(context, contact_id).await? {
413 res
415 } else {
416 let chat_id = ChatId::get_for_contact(context, contact_id).await?;
417 chat::set_muted(context, chat_id, MuteDuration::Forever).await?;
418 chat_id
419 };
420
421 Ok(chat_id)
422}
423
424async fn get_contact_stats(context: &Context, last_old_contact: u32) -> Result<Vec<ContactStat>> {
425 let mut verified_by_map: BTreeMap<ContactId, ContactId> = BTreeMap::new();
426 let mut bot_ids: BTreeSet<ContactId> = BTreeSet::new();
427
428 let mut contacts = context
429 .sql
430 .query_map_vec(
431 "SELECT id, fingerprint<>'', verifier, last_seen, is_bot FROM contacts c
432 WHERE id>9 AND origin>? AND addr<>?",
433 (Origin::Hidden, STATISTICS_BOT_EMAIL),
434 |row| {
435 let id = row.get(0)?;
436 let is_encrypted: bool = row.get(1)?;
437 let verifier: ContactId = row.get(2)?;
438 let last_seen: u64 = row.get(3)?;
439 let bot: bool = row.get(4)?;
440
441 let verified = match (is_encrypted, verifier) {
442 (true, ContactId::SELF) => VerifiedStatus::Direct,
443 (true, ContactId::UNDEFINED) => VerifiedStatus::Opportunistic,
444 (true, _) => VerifiedStatus::Transitive, (false, _) => VerifiedStatus::Unencrypted,
446 };
447
448 if verifier != ContactId::UNDEFINED {
449 verified_by_map.insert(id, verifier);
450 }
451
452 if bot {
453 bot_ids.insert(id);
454 }
455
456 Ok(ContactStat {
457 id,
458 verified,
459 bot,
460 direct_chat: false, last_seen,
462 transitive_chain: None, new: id.to_u32() > last_old_contact,
464 })
465 },
466 )
467 .await?;
468
469 for contact in &mut contacts {
471 if contact.verified == VerifiedStatus::Transitive {
472 let mut transitive_chain: u32 = 0;
473 let mut has_bot = false;
474 let mut current_verifier_id = contact.id;
475
476 while current_verifier_id != ContactId::SELF && transitive_chain < 100 {
477 current_verifier_id = match verified_by_map.get(¤t_verifier_id) {
478 Some(id) => *id,
479 None => {
480 transitive_chain = 0;
484 break;
485 }
486 };
487 if bot_ids.contains(¤t_verifier_id) {
488 has_bot = true;
489 }
490 transitive_chain = transitive_chain.saturating_add(1);
491 }
492
493 if transitive_chain > 0 {
494 contact.transitive_chain = Some(transitive_chain);
495 }
496
497 if has_bot {
498 contact.verified = VerifiedStatus::TransitiveViaBot;
499 }
500 }
501 }
502
503 for contact in &mut contacts {
505 let direct_chat = context
506 .sql
507 .exists(
508 "SELECT COUNT(*)
509 FROM chats_contacts cc INNER JOIN chats
510 WHERE cc.contact_id=? AND chats.type=?",
511 (contact.id, Chattype::Single),
512 )
513 .await?;
514 contact.direct_chat = direct_chat;
515 }
516
517 Ok(contacts)
518}
519
520async fn get_message_stats(context: &Context) -> Result<BTreeMap<Chattype, MessageStats>> {
525 let mut map: BTreeMap<Chattype, MessageStats> = context
526 .sql
527 .query_map_collect(
528 "SELECT chattype, verified, unverified_encrypted, unencrypted, only_to_self
529 FROM stats_msgs",
530 (),
531 |row| {
532 let chattype: Chattype = row.get(0)?;
533 let verified: u32 = row.get(1)?;
534 let unverified_encrypted: u32 = row.get(2)?;
535 let unencrypted: u32 = row.get(3)?;
536 let only_to_self: u32 = row.get(4)?;
537 let message_stats = MessageStats {
538 verified,
539 unverified_encrypted,
540 unencrypted,
541 only_to_self,
542 };
543 Ok((chattype, message_stats))
544 },
545 )
546 .await?;
547
548 for chattype in [Chattype::Group, Chattype::Single, Chattype::OutBroadcast] {
550 map.entry(chattype).or_default();
551 }
552
553 Ok(map)
554}
555
556pub(crate) async fn update_message_stats(context: &Context) -> Result<()> {
557 for chattype in [Chattype::Single, Chattype::Group, Chattype::OutBroadcast] {
558 update_message_stats_inner(context, chattype).await?;
559 }
560 context
561 .set_config_internal(Config::StatsLastUpdate, Some(&time().to_string()))
562 .await?;
563 Ok(())
564}
565
566async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Result<()> {
567 let stats_bot_chat_id = get_stats_chat_id(context).await?;
568
569 let trans_fn = |t: &mut rusqlite::Transaction| {
570 let last_counted_msg_id: u32 = t
573 .query_row(
574 "SELECT last_counted_msg_id FROM stats_msgs WHERE chattype=?",
575 (chattype,),
576 |row| row.get(0),
577 )
578 .optional()?
579 .unwrap_or(0);
580 t.execute(
581 "UPDATE stats_msgs
582 SET last_counted_msg_id=(SELECT MAX(id) FROM msgs)
583 WHERE chattype=?",
584 (chattype,),
585 )?;
586
587 t.execute(
591 "CREATE TEMP TABLE temp.empty_chats (
592 id INTEGER PRIMARY KEY
593 ) STRICT",
594 (),
595 )?;
596
597 t.execute(
600 "INSERT INTO temp.empty_chats
601 SELECT id FROM chats
602 WHERE id>9 AND NOT EXISTS(
603 SELECT *
604 FROM contacts, chats_contacts
605 WHERE chats_contacts.contact_id=contacts.id AND chats_contacts.chat_id=chats.id
606 AND contacts.id>9
607 )",
608 (),
609 )?;
610
611 t.execute(
614 "CREATE TEMP TABLE temp.verified_chats (
615 id INTEGER PRIMARY KEY
616 ) STRICT",
617 (),
618 )?;
619
620 t.execute(
623 "INSERT INTO temp.verified_chats
624 SELECT id FROM chats
625 WHERE id>9
626 AND id NOT IN (SELECT id FROM temp.empty_chats)
627 AND NOT EXISTS(
628 SELECT *
629 FROM contacts, chats_contacts
630 WHERE chats_contacts.contact_id=contacts.id AND chats_contacts.chat_id=chats.id
631 AND contacts.id>9
632 AND contacts.verifier=0
633 )",
634 (),
635 )?;
636
637 t.execute(
639 "CREATE TEMP TABLE temp.chat_with_correct_type (
640 id INTEGER PRIMARY KEY
641 ) STRICT",
642 (),
643 )?;
644
645 t.execute(
646 "INSERT INTO temp.chat_with_correct_type
647 SELECT id FROM chats
648 WHERE type=?;",
649 (chattype,),
650 )?;
651
652 let general_requirements = "id>? AND from_id=? AND chat_id<>?
659 AND hidden=0 AND chat_id>9 AND chat_id IN temp.chat_with_correct_type"
660 .to_string();
661 let params = (last_counted_msg_id, ContactId::SELF, stats_bot_chat_id);
662
663 let verified: u32 = t.query_row(
664 &format!(
665 "SELECT COUNT(*) FROM msgs
666 WHERE chat_id IN temp.verified_chats
667 AND {general_requirements}"
668 ),
669 params,
670 |row| row.get(0),
671 )?;
672
673 let unverified_encrypted: u32 = t.query_row(
674 &format!(
675 "SELECT COUNT(*) FROM msgs
677 WHERE chat_id NOT IN temp.verified_chats AND chat_id NOT IN temp.empty_chats
678 AND (param GLOB '*\nc=1*' OR param GLOB 'c=1*')
679 AND {general_requirements}"
680 ),
681 params,
682 |row| row.get(0),
683 )?;
684
685 let unencrypted: u32 = t.query_row(
686 &format!(
687 "SELECT COUNT(*) FROM msgs
688 WHERE chat_id NOT IN temp.verified_chats AND chat_id NOT IN temp.empty_chats
689 AND NOT (param GLOB '*\nc=1*' OR param GLOB 'c=1*')
690 AND {general_requirements}"
691 ),
692 params,
693 |row| row.get(0),
694 )?;
695
696 let only_to_self: u32 = t.query_row(
697 &format!(
698 "SELECT COUNT(*) FROM msgs
699 WHERE chat_id IN temp.empty_chats
700 AND {general_requirements}"
701 ),
702 params,
703 |row| row.get(0),
704 )?;
705
706 t.execute("DROP TABLE temp.verified_chats", ())?;
707 t.execute("DROP TABLE temp.empty_chats", ())?;
708 t.execute("DROP TABLE temp.chat_with_correct_type", ())?;
709
710 t.execute(
711 "INSERT INTO stats_msgs(chattype) VALUES (?)
712 ON CONFLICT(chattype) DO NOTHING",
713 (chattype,),
714 )?;
715 t.execute(
716 "UPDATE stats_msgs SET
717 verified=verified+?,
718 unverified_encrypted=unverified_encrypted+?,
719 unencrypted=unencrypted+?,
720 only_to_self=only_to_self+?
721 WHERE chattype=?",
722 (
723 verified,
724 unverified_encrypted,
725 unencrypted,
726 only_to_self,
727 chattype,
728 ),
729 )?;
730
731 Ok(())
732 };
733
734 context.sql.transaction(trans_fn).await?;
735
736 Ok(())
737}
738
739pub(crate) async fn count_securejoin_ux_info(
740 context: &Context,
741 source: Option<SecurejoinSource>,
742 uipath: Option<SecurejoinUiPath>,
743) -> Result<()> {
744 if !should_send_stats(context).await? {
745 return Ok(());
746 }
747
748 let source = source
749 .context("Missing securejoin source")
750 .log_err(context)
751 .unwrap_or(SecurejoinSource::Unknown);
752
753 let uipath = uipath.unwrap_or(SecurejoinUiPath::Unknown);
757
758 context
759 .sql
760 .transaction(|conn| {
761 conn.execute(
762 "INSERT INTO stats_securejoin_sources VALUES (?, 1)
763 ON CONFLICT (source) DO UPDATE SET count=count+1;",
764 (source.to_u32(),),
765 )?;
766
767 conn.execute(
768 "INSERT INTO stats_securejoin_uipaths VALUES (?, 1)
769 ON CONFLICT (uipath) DO UPDATE SET count=count+1;",
770 (uipath.to_u32(),),
771 )?;
772 Ok(())
773 })
774 .await?;
775
776 Ok(())
777}
778
779async fn get_securejoin_source_stats(context: &Context) -> Result<SecurejoinSources> {
780 let map: BTreeMap<SecurejoinSource, u32> = context
781 .sql
782 .query_map_collect(
783 "SELECT source, count FROM stats_securejoin_sources",
784 (),
785 |row| {
786 let source: SecurejoinSource = row.get(0)?;
787 let count: u32 = row.get(1)?;
788 Ok((source, count))
789 },
790 )
791 .await?;
792
793 let stats = SecurejoinSources {
794 unknown: *map.get(&SecurejoinSource::Unknown).unwrap_or(&0),
795 external_link: *map.get(&SecurejoinSource::ExternalLink).unwrap_or(&0),
796 internal_link: *map.get(&SecurejoinSource::InternalLink).unwrap_or(&0),
797 clipboard: *map.get(&SecurejoinSource::Clipboard).unwrap_or(&0),
798 image_loaded: *map.get(&SecurejoinSource::ImageLoaded).unwrap_or(&0),
799 scan: *map.get(&SecurejoinSource::Scan).unwrap_or(&0),
800 };
801
802 Ok(stats)
803}
804
805async fn get_securejoin_uipath_stats(context: &Context) -> Result<SecurejoinUiPaths> {
806 let map: BTreeMap<SecurejoinUiPath, u32> = context
807 .sql
808 .query_map_collect(
809 "SELECT uipath, count FROM stats_securejoin_uipaths",
810 (),
811 |row| {
812 let uipath: SecurejoinUiPath = row.get(0)?;
813 let count: u32 = row.get(1)?;
814 Ok((uipath, count))
815 },
816 )
817 .await?;
818
819 let stats = SecurejoinUiPaths {
820 other: *map.get(&SecurejoinUiPath::Unknown).unwrap_or(&0),
821 qr_icon: *map.get(&SecurejoinUiPath::QrIcon).unwrap_or(&0),
822 new_contact: *map.get(&SecurejoinUiPath::NewContact).unwrap_or(&0),
823 };
824
825 Ok(stats)
826}
827
828pub(crate) async fn count_securejoin_invite(context: &Context, invite: &QrInvite) -> Result<()> {
829 if !should_send_stats(context).await? {
830 return Ok(());
831 }
832
833 let contact = Contact::get_by_id(context, invite.contact_id()).await?;
834
835 let already_existed = contact.origin > Origin::UnhandledSecurejoinQrScan;
841
842 let already_verified = contact.is_verified(context).await?;
844
845 let typ = match invite {
846 QrInvite::Contact { .. } => "contact",
847 QrInvite::Group { .. } => "group",
848 QrInvite::Broadcast { .. } => "broadcast",
849 };
850
851 context
852 .sql
853 .execute(
854 "INSERT INTO stats_securejoin_invites (already_existed, already_verified, type)
855 VALUES (?, ?, ?)",
856 (already_existed, already_verified, typ),
857 )
858 .await?;
859
860 Ok(())
861}
862
863async fn get_securejoin_invite_stats(context: &Context) -> Result<Vec<JoinedInvite>> {
864 context
865 .sql
866 .query_map_vec(
867 "SELECT already_existed, already_verified, type FROM stats_securejoin_invites",
868 (),
869 |row| {
870 let already_existed: bool = row.get(0)?;
871 let already_verified: bool = row.get(1)?;
872 let typ: String = row.get(2)?;
873
874 Ok(JoinedInvite {
875 already_existed,
876 already_verified,
877 typ,
878 })
879 },
880 )
881 .await
882}
883
884#[cfg(test)]
885mod stats_tests;