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