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,
162}
163
164pub(crate) async fn pre_sending_config_change(
165 context: &Context,
166 old_value: bool,
167 new_value: bool,
168) -> Result<()> {
169 ensure_last_old_contact_id(context).await?;
172 stats_id(context).await?;
175
176 if old_value != new_value {
177 if new_value {
178 set_last_counted_msg_id(context).await?;
180 } else {
181 update_message_stats(context).await?;
183 }
184
185 let sql_table = if new_value {
186 "stats_sending_enabled_events"
187 } else {
188 "stats_sending_disabled_events"
189 };
190
191 context
192 .sql
193 .execute(&format!("INSERT INTO {sql_table} VALUES(?)"), (time(),))
194 .await?;
195 }
196
197 Ok(())
198}
199
200pub async fn maybe_send_stats(context: &Context) -> Result<Option<ChatId>> {
207 if should_send_stats(context).await?
208 && time_has_passed(context, Config::StatsLastSent, SENDING_INTERVAL_SECONDS).await?
209 {
210 let chat_id = send_stats(context).await?;
211
212 return Ok(Some(chat_id));
213 }
214 Ok(None)
215}
216
217pub(crate) async fn maybe_update_message_stats(context: &Context) -> Result<()> {
218 if should_send_stats(context).await?
219 && time_has_passed(
220 context,
221 Config::StatsLastUpdate,
222 MESSAGE_STATS_UPDATE_INTERVAL_SECONDS,
223 )
224 .await?
225 {
226 update_message_stats(context).await?;
227 }
228
229 Ok(())
230}
231
232async fn time_has_passed(context: &Context, config: Config, seconds: i64) -> Result<bool> {
233 let last_time = context.get_config_i64(config).await?;
234 let next_time = last_time.saturating_add(seconds);
235
236 let res = if next_time <= time() {
237 context
240 .set_config_internal(config, Some(&time().to_string()))
241 .await?;
242 true
243 } else {
244 if time() < last_time {
245 context
249 .set_config_internal(config, Some(&time().to_string()))
250 .await?;
251 }
252 false
253 };
254
255 Ok(res)
256}
257
258#[allow(clippy::unused_async, unused)]
259pub(crate) async fn should_send_stats(context: &Context) -> Result<bool> {
260 #[cfg(any(target_os = "android", test))]
261 {
262 context.get_config_bool(Config::StatsSending).await
263 }
264
265 #[cfg(not(any(target_os = "android", test)))]
269 {
270 Ok(false)
271 }
272}
273
274async fn send_stats(context: &Context) -> Result<ChatId> {
275 info!(context, "Sending statistics.");
276
277 update_message_stats(context).await?;
278
279 let chat_id = get_stats_chat_id(context).await?;
280
281 let mut msg = Message::new(Viewtype::File);
282 msg.set_text(crate::stock_str::stats_msg_body(context).await);
283
284 let stats = get_stats(context).await?;
285
286 msg.set_file_from_bytes(
287 context,
288 "statistics.txt",
289 stats.as_bytes(),
290 Some("text/plain"),
291 )?;
292
293 chat::send_msg(context, chat_id, &mut msg)
294 .await
295 .context("Failed to send statistics message")
296 .log_err(context)
297 .ok();
298
299 Ok(chat_id)
300}
301
302async fn set_last_counted_msg_id(context: &Context) -> Result<()> {
303 context
304 .sql
305 .execute(
306 "UPDATE stats_msgs
307 SET last_counted_msg_id=(SELECT MAX(id) FROM msgs)",
308 (),
309 )
310 .await?;
311
312 Ok(())
313}
314
315async fn ensure_last_old_contact_id(context: &Context) -> Result<()> {
316 if context.config_exists(Config::StatsLastOldContactId).await? {
317 return Ok(());
320 }
321
322 let last_contact_id: u64 = context
323 .sql
324 .query_get_value("SELECT MAX(id) FROM contacts", ())
325 .await?
326 .unwrap_or(0);
327
328 context
329 .sql
330 .set_raw_config(
331 Config::StatsLastOldContactId.as_ref(),
332 Some(&last_contact_id.to_string()),
333 )
334 .await?;
335
336 Ok(())
337}
338
339async fn get_stats(context: &Context) -> Result<String> {
340 let last_old_contact = context
343 .get_config_u32(Config::StatsLastOldContactId)
344 .await?;
345
346 let key_create_timestamps: Vec<i64> = load_self_public_keyring(context)
347 .await?
348 .iter()
349 .map(|k| k.created_at().timestamp())
350 .collect();
351
352 let sending_enabled_timestamps =
353 get_timestamps(context, "stats_sending_enabled_events").await?;
354 let sending_disabled_timestamps =
355 get_timestamps(context, "stats_sending_disabled_events").await?;
356
357 let stats = Statistics {
358 core_version: get_version_str().to_string(),
359 key_create_timestamps,
360 stats_id: stats_id(context).await?,
361 is_chatmail: context.is_chatmail().await?,
362 contact_stats: get_contact_stats(context, last_old_contact).await?,
363 message_stats: get_message_stats(context).await?,
364 securejoin_sources: get_securejoin_source_stats(context).await?,
365 securejoin_uipaths: get_securejoin_uipath_stats(context).await?,
366 securejoin_invites: get_securejoin_invite_stats(context).await?,
367 sending_enabled_timestamps,
368 sending_disabled_timestamps,
369 };
370
371 Ok(serde_json::to_string_pretty(&stats)?)
372}
373
374async fn get_timestamps(context: &Context, sql_table: &str) -> Result<Vec<i64>> {
375 context
376 .sql
377 .query_map_vec(
378 &format!("SELECT timestamp FROM {sql_table} LIMIT 1000"),
379 (),
380 |row| row.get(0),
381 )
382 .await
383}
384
385pub(crate) async fn stats_id(context: &Context) -> Result<String> {
386 Ok(match context.get_config(Config::StatsId).await? {
387 Some(id) => id,
388 None => {
389 let id = create_id();
390 context
391 .set_config_internal(Config::StatsId, Some(&id))
392 .await?;
393 id
394 }
395 })
396}
397
398async fn get_stats_chat_id(context: &Context) -> Result<ChatId, anyhow::Error> {
399 let contact_id: ContactId = *import_vcard(context, STATISTICS_BOT_VCARD)
400 .await?
401 .first()
402 .context("Statistics bot vCard does not contain a contact")?;
403 mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
404
405 let chat_id = if let Some(res) = ChatId::lookup_by_contact(context, contact_id).await? {
406 res
408 } else {
409 let chat_id = ChatId::get_for_contact(context, contact_id).await?;
410 chat::set_muted(context, chat_id, MuteDuration::Forever).await?;
411 chat_id
412 };
413
414 Ok(chat_id)
415}
416
417async fn get_contact_stats(context: &Context, last_old_contact: u32) -> Result<Vec<ContactStat>> {
418 let mut verified_by_map: BTreeMap<ContactId, ContactId> = BTreeMap::new();
419 let mut bot_ids: BTreeSet<ContactId> = BTreeSet::new();
420
421 let mut contacts = context
422 .sql
423 .query_map_vec(
424 "SELECT id, fingerprint<>'', verifier, last_seen, is_bot FROM contacts c
425 WHERE id>9 AND origin>? AND addr<>?",
426 (Origin::Hidden, STATISTICS_BOT_EMAIL),
427 |row| {
428 let id = row.get(0)?;
429 let is_encrypted: bool = row.get(1)?;
430 let verifier: ContactId = row.get(2)?;
431 let last_seen: u64 = row.get(3)?;
432 let bot: bool = row.get(4)?;
433
434 let verified = match (is_encrypted, verifier) {
435 (true, ContactId::SELF) => VerifiedStatus::Direct,
436 (true, ContactId::UNDEFINED) => VerifiedStatus::Opportunistic,
437 (true, _) => VerifiedStatus::Transitive, (false, _) => VerifiedStatus::Unencrypted,
439 };
440
441 if verifier != ContactId::UNDEFINED {
442 verified_by_map.insert(id, verifier);
443 }
444
445 if bot {
446 bot_ids.insert(id);
447 }
448
449 Ok(ContactStat {
450 id,
451 verified,
452 bot,
453 direct_chat: false, last_seen,
455 transitive_chain: None, new: id.to_u32() > last_old_contact,
457 })
458 },
459 )
460 .await?;
461
462 for contact in &mut contacts {
464 if contact.verified == VerifiedStatus::Transitive {
465 let mut transitive_chain: u32 = 0;
466 let mut has_bot = false;
467 let mut current_verifier_id = contact.id;
468
469 while current_verifier_id != ContactId::SELF && transitive_chain < 100 {
470 current_verifier_id = match verified_by_map.get(¤t_verifier_id) {
471 Some(id) => *id,
472 None => {
473 transitive_chain = 0;
477 break;
478 }
479 };
480 if bot_ids.contains(¤t_verifier_id) {
481 has_bot = true;
482 }
483 transitive_chain = transitive_chain.saturating_add(1);
484 }
485
486 if transitive_chain > 0 {
487 contact.transitive_chain = Some(transitive_chain);
488 }
489
490 if has_bot {
491 contact.verified = VerifiedStatus::TransitiveViaBot;
492 }
493 }
494 }
495
496 for contact in &mut contacts {
498 let direct_chat = context
499 .sql
500 .exists(
501 "SELECT COUNT(*)
502 FROM chats_contacts cc INNER JOIN chats
503 WHERE cc.contact_id=? AND chats.type=?",
504 (contact.id, Chattype::Single),
505 )
506 .await?;
507 contact.direct_chat = direct_chat;
508 }
509
510 Ok(contacts)
511}
512
513async fn get_message_stats(context: &Context) -> Result<BTreeMap<Chattype, MessageStats>> {
518 let mut map: BTreeMap<Chattype, MessageStats> = context
519 .sql
520 .query_map_collect(
521 "SELECT chattype, verified, unverified_encrypted, unencrypted, only_to_self
522 FROM stats_msgs",
523 (),
524 |row| {
525 let chattype: Chattype = row.get(0)?;
526 let verified: u32 = row.get(1)?;
527 let unverified_encrypted: u32 = row.get(2)?;
528 let unencrypted: u32 = row.get(3)?;
529 let only_to_self: u32 = row.get(4)?;
530 let message_stats = MessageStats {
531 verified,
532 unverified_encrypted,
533 unencrypted,
534 only_to_self,
535 };
536 Ok((chattype, message_stats))
537 },
538 )
539 .await?;
540
541 for chattype in [Chattype::Group, Chattype::Single, Chattype::OutBroadcast] {
543 map.entry(chattype).or_default();
544 }
545
546 Ok(map)
547}
548
549pub(crate) async fn update_message_stats(context: &Context) -> Result<()> {
550 for chattype in [Chattype::Single, Chattype::Group, Chattype::OutBroadcast] {
551 update_message_stats_inner(context, chattype).await?;
552 }
553 context
554 .set_config_internal(Config::StatsLastUpdate, Some(&time().to_string()))
555 .await?;
556 Ok(())
557}
558
559async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Result<()> {
560 let stats_bot_chat_id = get_stats_chat_id(context).await?;
561
562 let trans_fn = |t: &mut rusqlite::Transaction| {
563 let last_counted_msg_id: u32 = t
566 .query_row(
567 "SELECT last_counted_msg_id FROM stats_msgs WHERE chattype=?",
568 (chattype,),
569 |row| row.get(0),
570 )
571 .optional()?
572 .unwrap_or(0);
573 t.execute(
574 "UPDATE stats_msgs
575 SET last_counted_msg_id=(SELECT MAX(id) FROM msgs)
576 WHERE chattype=?",
577 (chattype,),
578 )?;
579
580 t.execute(
584 "CREATE TEMP TABLE temp.empty_chats (
585 id INTEGER PRIMARY KEY
586 ) STRICT",
587 (),
588 )?;
589
590 t.execute(
593 "INSERT INTO temp.empty_chats
594 SELECT id FROM chats
595 WHERE id>9 AND NOT EXISTS(
596 SELECT *
597 FROM contacts, chats_contacts
598 WHERE chats_contacts.contact_id=contacts.id AND chats_contacts.chat_id=chats.id
599 AND contacts.id>9
600 )",
601 (),
602 )?;
603
604 t.execute(
607 "CREATE TEMP TABLE temp.verified_chats (
608 id INTEGER PRIMARY KEY
609 ) STRICT",
610 (),
611 )?;
612
613 t.execute(
616 "INSERT INTO temp.verified_chats
617 SELECT id FROM chats
618 WHERE id>9
619 AND id NOT IN (SELECT id FROM temp.empty_chats)
620 AND NOT EXISTS(
621 SELECT *
622 FROM contacts, chats_contacts
623 WHERE chats_contacts.contact_id=contacts.id AND chats_contacts.chat_id=chats.id
624 AND contacts.id>9
625 AND contacts.verifier=0
626 )",
627 (),
628 )?;
629
630 t.execute(
632 "CREATE TEMP TABLE temp.chat_with_correct_type (
633 id INTEGER PRIMARY KEY
634 ) STRICT",
635 (),
636 )?;
637
638 t.execute(
639 "INSERT INTO temp.chat_with_correct_type
640 SELECT id FROM chats
641 WHERE type=?;",
642 (chattype,),
643 )?;
644
645 let general_requirements = "id>? AND from_id=? AND chat_id<>?
652 AND hidden=0 AND chat_id>9 AND chat_id IN temp.chat_with_correct_type"
653 .to_string();
654 let params = (last_counted_msg_id, ContactId::SELF, stats_bot_chat_id);
655
656 let verified: u32 = t.query_row(
657 &format!(
658 "SELECT COUNT(*) FROM msgs
659 WHERE chat_id IN temp.verified_chats
660 AND {general_requirements}"
661 ),
662 params,
663 |row| row.get(0),
664 )?;
665
666 let unverified_encrypted: u32 = t.query_row(
667 &format!(
668 "SELECT COUNT(*) FROM msgs
670 WHERE chat_id NOT IN temp.verified_chats AND chat_id NOT IN temp.empty_chats
671 AND (param GLOB '*\nc=1*' OR param GLOB 'c=1*')
672 AND {general_requirements}"
673 ),
674 params,
675 |row| row.get(0),
676 )?;
677
678 let unencrypted: u32 = t.query_row(
679 &format!(
680 "SELECT COUNT(*) FROM msgs
681 WHERE chat_id NOT IN temp.verified_chats AND chat_id NOT IN temp.empty_chats
682 AND NOT (param GLOB '*\nc=1*' OR param GLOB 'c=1*')
683 AND {general_requirements}"
684 ),
685 params,
686 |row| row.get(0),
687 )?;
688
689 let only_to_self: u32 = t.query_row(
690 &format!(
691 "SELECT COUNT(*) FROM msgs
692 WHERE chat_id IN temp.empty_chats
693 AND {general_requirements}"
694 ),
695 params,
696 |row| row.get(0),
697 )?;
698
699 t.execute("DROP TABLE temp.verified_chats", ())?;
700 t.execute("DROP TABLE temp.empty_chats", ())?;
701 t.execute("DROP TABLE temp.chat_with_correct_type", ())?;
702
703 t.execute(
704 "INSERT INTO stats_msgs(chattype) VALUES (?)
705 ON CONFLICT(chattype) DO NOTHING",
706 (chattype,),
707 )?;
708 t.execute(
709 "UPDATE stats_msgs SET
710 verified=verified+?,
711 unverified_encrypted=unverified_encrypted+?,
712 unencrypted=unencrypted+?,
713 only_to_self=only_to_self+?
714 WHERE chattype=?",
715 (
716 verified,
717 unverified_encrypted,
718 unencrypted,
719 only_to_self,
720 chattype,
721 ),
722 )?;
723
724 Ok(())
725 };
726
727 context.sql.transaction(trans_fn).await?;
728
729 Ok(())
730}
731
732pub(crate) async fn count_securejoin_ux_info(
733 context: &Context,
734 source: Option<SecurejoinSource>,
735 uipath: Option<SecurejoinUiPath>,
736) -> Result<()> {
737 if !should_send_stats(context).await? {
738 return Ok(());
739 }
740
741 let source = source
742 .context("Missing securejoin source")
743 .log_err(context)
744 .unwrap_or(SecurejoinSource::Unknown);
745
746 let uipath = uipath.unwrap_or(SecurejoinUiPath::Unknown);
750
751 context
752 .sql
753 .transaction(|conn| {
754 conn.execute(
755 "INSERT INTO stats_securejoin_sources VALUES (?, 1)
756 ON CONFLICT (source) DO UPDATE SET count=count+1;",
757 (source.to_u32(),),
758 )?;
759
760 conn.execute(
761 "INSERT INTO stats_securejoin_uipaths VALUES (?, 1)
762 ON CONFLICT (uipath) DO UPDATE SET count=count+1;",
763 (uipath.to_u32(),),
764 )?;
765 Ok(())
766 })
767 .await?;
768
769 Ok(())
770}
771
772async fn get_securejoin_source_stats(context: &Context) -> Result<SecurejoinSources> {
773 let map: BTreeMap<SecurejoinSource, u32> = context
774 .sql
775 .query_map_collect(
776 "SELECT source, count FROM stats_securejoin_sources",
777 (),
778 |row| {
779 let source: SecurejoinSource = row.get(0)?;
780 let count: u32 = row.get(1)?;
781 Ok((source, count))
782 },
783 )
784 .await?;
785
786 let stats = SecurejoinSources {
787 unknown: *map.get(&SecurejoinSource::Unknown).unwrap_or(&0),
788 external_link: *map.get(&SecurejoinSource::ExternalLink).unwrap_or(&0),
789 internal_link: *map.get(&SecurejoinSource::InternalLink).unwrap_or(&0),
790 clipboard: *map.get(&SecurejoinSource::Clipboard).unwrap_or(&0),
791 image_loaded: *map.get(&SecurejoinSource::ImageLoaded).unwrap_or(&0),
792 scan: *map.get(&SecurejoinSource::Scan).unwrap_or(&0),
793 };
794
795 Ok(stats)
796}
797
798async fn get_securejoin_uipath_stats(context: &Context) -> Result<SecurejoinUiPaths> {
799 let map: BTreeMap<SecurejoinUiPath, u32> = context
800 .sql
801 .query_map_collect(
802 "SELECT uipath, count FROM stats_securejoin_uipaths",
803 (),
804 |row| {
805 let uipath: SecurejoinUiPath = row.get(0)?;
806 let count: u32 = row.get(1)?;
807 Ok((uipath, count))
808 },
809 )
810 .await?;
811
812 let stats = SecurejoinUiPaths {
813 other: *map.get(&SecurejoinUiPath::Unknown).unwrap_or(&0),
814 qr_icon: *map.get(&SecurejoinUiPath::QrIcon).unwrap_or(&0),
815 new_contact: *map.get(&SecurejoinUiPath::NewContact).unwrap_or(&0),
816 };
817
818 Ok(stats)
819}
820
821pub(crate) async fn count_securejoin_invite(context: &Context, invite: &QrInvite) -> Result<()> {
822 if !should_send_stats(context).await? {
823 return Ok(());
824 }
825
826 let contact = Contact::get_by_id(context, invite.contact_id()).await?;
827
828 let already_existed = contact.origin > Origin::UnhandledSecurejoinQrScan;
834
835 let already_verified = contact.is_verified(context).await?;
837
838 let typ = match invite {
839 QrInvite::Contact { .. } => "contact",
840 QrInvite::Group { .. } => "group",
841 };
842
843 context
844 .sql
845 .execute(
846 "INSERT INTO stats_securejoin_invites (already_existed, already_verified, type)
847 VALUES (?, ?, ?)",
848 (already_existed, already_verified, typ),
849 )
850 .await?;
851
852 Ok(())
853}
854
855async fn get_securejoin_invite_stats(context: &Context) -> Result<Vec<JoinedInvite>> {
856 context
857 .sql
858 .query_map_vec(
859 "SELECT already_existed, already_verified, type FROM stats_securejoin_invites",
860 (),
861 |row| {
862 let already_existed: bool = row.get(0)?;
863 let already_verified: bool = row.get(1)?;
864 let typ: String = row.get(2)?;
865
866 Ok(JoinedInvite {
867 already_existed,
868 already_verified,
869 typ,
870 })
871 },
872 )
873 .await
874}
875
876#[cfg(test)]
877mod stats_tests;