Skip to main content

deltachat/
stats.rs

1//! Delta Chat has an advanced option
2//! "Send statistics to the developers of Delta Chat".
3//! If this is enabled, a JSON file with some anonymous statistics
4//! will be sent to a bot once a week.
5
6use std::collections::{BTreeMap, BTreeSet};
7
8use anyhow::{Context as _, Result};
9use deltachat_derive::FromSql;
10use num_traits::ToPrimitive;
11use pgp::types::KeyDetails as _;
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::{DcKey, load_self_public_key};
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; // 1 week
30// const SENDING_INTERVAL_SECONDS: i64 = 60; // 1 minute (for testing)
31const MESSAGE_STATS_UPDATE_INTERVAL_SECONDS: i64 = 4 * 60; // 4 minutes (less than the lowest ephemeral messages timeout)
32
33#[derive(Serialize)]
34struct Statistics {
35    core_version: String,
36    number_of_transports: usize,
37    key_create_timestamps: Vec<u32>,
38    number_of_keys: u32,
39    /// OpenPGP version of the key.
40    key_version: u8,
41    key_algorithm: String,
42    /// Size of the public key in bytes (encoded in binary, not base64).
43    pubkey_size: usize,
44    stats_id: String,
45    is_chatmail: bool,
46    contact_stats: Vec<ContactStat>,
47    message_stats: BTreeMap<Chattype, MessageStats>,
48    securejoin_sources: SecurejoinSources,
49    securejoin_uipaths: SecurejoinUiPaths,
50    securejoin_invites: Vec<JoinedInvite>,
51    sending_enabled_timestamps: Vec<i64>,
52    sending_disabled_timestamps: Vec<i64>,
53}
54
55#[derive(Serialize, PartialEq)]
56enum VerifiedStatus {
57    Direct,
58    Transitive,
59    TransitiveViaBot,
60    Opportunistic,
61    Unencrypted,
62}
63
64#[derive(Serialize)]
65struct ContactStat {
66    #[serde(skip_serializing)]
67    id: ContactId,
68
69    verified: VerifiedStatus,
70
71    // If one of the boolean properties is false,
72    // we leave them away.
73    // This way, the Json file becomes a lot smaller.
74    #[serde(skip_serializing_if = "is_false")]
75    bot: bool,
76
77    #[serde(skip_serializing_if = "is_false")]
78    direct_chat: bool,
79
80    last_seen: u64,
81
82    #[serde(skip_serializing_if = "Option::is_none")]
83    transitive_chain: Option<u32>,
84
85    /// Whether the contact was established after stats-sending was enabled
86    #[serde(skip_serializing_if = "is_false")]
87    new: bool,
88}
89
90fn is_false(b: &bool) -> bool {
91    !b
92}
93
94#[derive(Serialize, Default)]
95struct MessageStats {
96    verified: u32,
97    unverified_encrypted: u32,
98    unencrypted: u32,
99    only_to_self: u32,
100}
101
102/// Where a securejoin invite link or QR code came from.
103/// This is only used if the user enabled StatsSending.
104#[repr(u32)]
105#[derive(
106    Debug, Clone, Copy, ToPrimitive, FromPrimitive, FromSql, PartialEq, Eq, PartialOrd, Ord,
107)]
108pub enum SecurejoinSource {
109    /// Because of some problem, it is unknown where the QR code came from.
110    Unknown = 0,
111    /// The user opened a link somewhere outside Delta Chat
112    ExternalLink = 1,
113    /// The user clicked on a link in a message inside Delta Chat
114    InternalLink = 2,
115    /// The user clicked "Paste from Clipboard" in the QR scan activity
116    Clipboard = 3,
117    /// The user clicked "Load QR code as image" in the QR scan activity
118    ImageLoaded = 4,
119    /// The user scanned a QR code
120    Scan = 5,
121}
122
123#[derive(Serialize)]
124struct SecurejoinSources {
125    unknown: u32,
126    external_link: u32,
127    internal_link: u32,
128    clipboard: u32,
129    image_loaded: u32,
130    scan: u32,
131}
132
133/// How the user opened the QR activity in order scan a QR code on Android.
134/// This is only used if the user enabled StatsSending.
135#[derive(
136    Debug, Clone, Copy, ToPrimitive, FromPrimitive, FromSql, PartialEq, Eq, PartialOrd, Ord,
137)]
138pub enum SecurejoinUiPath {
139    /// The UI path is unknown, or the user didn't open the QR code screen at all.
140    Unknown = 0,
141    /// The user directly clicked on the QR icon in the main screen
142    QrIcon = 1,
143    /// The user first clicked on the `+` button in the main screen,
144    /// and then on "New Contact"
145    NewContact = 2,
146}
147
148#[derive(Serialize)]
149struct SecurejoinUiPaths {
150    other: u32,
151    qr_icon: u32,
152    new_contact: u32,
153}
154
155/// Some information on an invite-joining event
156/// (i.e. a qr scan or a clicked link).
157#[derive(Serialize)]
158struct JoinedInvite {
159    /// Whether the contact already existed before.
160    /// If this is false, then a contact was newly created.
161    already_existed: bool,
162    /// If a contact already existed,
163    /// this tells us whether the contact was verified already.
164    already_verified: bool,
165    /// The type of the invite:
166    /// "contact" for 1:1 invites that setup a verified contact,
167    /// "group" for invites that invite to a group,
168    /// "broadcast" for invites that invite to a broadcast channel.
169    /// The invite also performs the contact verification 'along the way'.
170    typ: String,
171}
172
173pub(crate) async fn pre_sending_config_change(
174    context: &Context,
175    old_value: bool,
176    new_value: bool,
177) -> Result<()> {
178    // These functions are no-ops if they were called in the past already;
179    // just call them opportunistically:
180    ensure_last_old_contact_id(context).await?;
181    // Make sure that StatsId is available for the UI,
182    // in order to open the survey with the StatsId as a parameter:
183    stats_id(context).await?;
184
185    if old_value != new_value {
186        if new_value {
187            // Only count messages sent from now on:
188            set_last_counted_msg_id(context).await?;
189        } else {
190            // Update message stats one last time in case it's enabled again in the future:
191            update_message_stats(context).await?;
192        }
193
194        let sql_table = if new_value {
195            "stats_sending_enabled_events"
196        } else {
197            "stats_sending_disabled_events"
198        };
199
200        context
201            .sql
202            .execute(&format!("INSERT INTO {sql_table} VALUES(?)"), (time(),))
203            .await?;
204    }
205
206    Ok(())
207}
208
209/// Sends a message with statistics about the usage of Delta Chat,
210/// if the last time such a message was sent
211/// was more than a week ago.
212///
213/// On the other end, a bot will receive the message and make it available
214/// to Delta Chat's developers.
215pub async fn maybe_send_stats(context: &Context) -> Result<Option<ChatId>> {
216    if should_send_stats(context).await?
217        && time_has_passed(context, Config::StatsLastSent, SENDING_INTERVAL_SECONDS).await?
218    {
219        let chat_id = send_stats(context).await?;
220
221        return Ok(Some(chat_id));
222    }
223    Ok(None)
224}
225
226pub(crate) async fn maybe_update_message_stats(context: &Context) -> Result<()> {
227    if should_send_stats(context).await?
228        && time_has_passed(
229            context,
230            Config::StatsLastUpdate,
231            MESSAGE_STATS_UPDATE_INTERVAL_SECONDS,
232        )
233        .await?
234    {
235        update_message_stats(context).await?;
236    }
237
238    Ok(())
239}
240
241async fn time_has_passed(context: &Context, config: Config, seconds: i64) -> Result<bool> {
242    let last_time = context.get_config_i64(config).await?;
243    let next_time = last_time.saturating_add(seconds);
244
245    let res = if next_time <= time() {
246        // Already set the config to the current time.
247        // This prevents infinite loops in the (unlikely) case of an error:
248        context
249            .set_config_internal(config, Some(&time().to_string()))
250            .await?;
251        true
252    } else {
253        if time() < last_time {
254            // The clock was rewound.
255            // Reset the config, so that the statistics will be sent normally in a week,
256            // or be normally updated in a few minutes.
257            context
258                .set_config_internal(config, Some(&time().to_string()))
259                .await?;
260        }
261        false
262    };
263
264    Ok(res)
265}
266
267#[allow(clippy::unused_async, unused)]
268pub(crate) async fn should_send_stats(context: &Context) -> Result<bool> {
269    #[cfg(any(target_os = "android", test))]
270    {
271        context.get_config_bool(Config::StatsSending).await
272    }
273
274    // If the user enables statistics-sending on Android,
275    // and then transfers the account to e.g. Desktop,
276    // we should not send any statistics:
277    #[cfg(not(any(target_os = "android", test)))]
278    {
279        Ok(false)
280    }
281}
282
283async fn send_stats(context: &Context) -> Result<ChatId> {
284    info!(context, "Sending statistics.");
285
286    update_message_stats(context).await?;
287
288    let chat_id = get_stats_chat_id(context).await?;
289
290    let mut msg = Message::new(Viewtype::File);
291    msg.set_text(crate::stock_str::stats_msg_body(context));
292
293    let stats = get_stats(context).await?;
294
295    msg.set_file_from_bytes(
296        context,
297        "statistics.txt",
298        stats.as_bytes(),
299        Some("text/plain"),
300    )?;
301
302    chat::send_msg(context, chat_id, &mut msg)
303        .await
304        .context("Failed to send statistics message")
305        .log_err(context)
306        .ok();
307
308    Ok(chat_id)
309}
310
311async fn set_last_counted_msg_id(context: &Context) -> Result<()> {
312    context
313        .sql
314        .execute(
315            "UPDATE stats_msgs
316            SET last_counted_msg_id=(SELECT MAX(id) FROM msgs)",
317            (),
318        )
319        .await?;
320
321    Ok(())
322}
323
324async fn ensure_last_old_contact_id(context: &Context) -> Result<()> {
325    if context.config_exists(Config::StatsLastOldContactId).await? {
326        // The user had statistics-sending enabled already in the past,
327        // keep the 'last old contact id' as-is
328        return Ok(());
329    }
330
331    let last_contact_id: u64 = context
332        .sql
333        .query_get_value("SELECT MAX(id) FROM contacts", ())
334        .await?
335        .unwrap_or(0);
336
337    context
338        .sql
339        .set_raw_config(
340            Config::StatsLastOldContactId.as_ref(),
341            Some(&last_contact_id.to_string()),
342        )
343        .await?;
344
345    Ok(())
346}
347
348async fn get_stats(context: &Context) -> Result<String> {
349    // The Id of the last contact that already existed when the user enabled the setting.
350    // Newer contacts will get the `new` flag set.
351    let last_old_contact = context
352        .get_config_u32(Config::StatsLastOldContactId)
353        .await?;
354
355    let self_public_key = load_self_public_key(context).await?;
356    // `key_create_timestamps` is a `Vec` for historical reasons,
357    // support for using multiple keys is being phased out.
358    let key_create_timestamps: Vec<u32> = vec![self_public_key.created_at().as_secs()];
359    let number_of_keys: u32 = context
360        .sql
361        .query_get_value("SELECT COUNT(*) FROM keypairs", ())
362        .await?
363        .unwrap_or(0);
364
365    let sending_enabled_timestamps =
366        get_timestamps(context, "stats_sending_enabled_events").await?;
367    let sending_disabled_timestamps =
368        get_timestamps(context, "stats_sending_disabled_events").await?;
369
370    let stats = Statistics {
371        core_version: DC_VERSION_STR.to_string(),
372        number_of_transports: context.count_transports().await?,
373        key_create_timestamps,
374        number_of_keys,
375        key_version: self_public_key.primary_key.version().into(),
376        key_algorithm: format!("{:?}", self_public_key.algorithm()),
377        pubkey_size: DcKey::to_bytes(&self_public_key).len(),
378        stats_id: stats_id(context).await?,
379        is_chatmail: context.is_chatmail().await?,
380        contact_stats: get_contact_stats(context, last_old_contact).await?,
381        message_stats: get_message_stats(context).await?,
382        securejoin_sources: get_securejoin_source_stats(context).await?,
383        securejoin_uipaths: get_securejoin_uipath_stats(context).await?,
384        securejoin_invites: get_securejoin_invite_stats(context).await?,
385        sending_enabled_timestamps,
386        sending_disabled_timestamps,
387    };
388
389    Ok(serde_json::to_string_pretty(&stats)?)
390}
391
392async fn get_timestamps(context: &Context, sql_table: &str) -> Result<Vec<i64>> {
393    context
394        .sql
395        .query_map_vec(
396            &format!("SELECT timestamp FROM {sql_table} LIMIT 1000"),
397            (),
398            |row| {
399                let timestamp: i64 = row.get(0)?;
400                Ok(timestamp)
401            },
402        )
403        .await
404}
405
406pub(crate) async fn stats_id(context: &Context) -> Result<String> {
407    Ok(match context.get_config(Config::StatsId).await? {
408        Some(id) => id,
409        None => {
410            let id = rand::distr::Alphabetic
411                .sample_string(&mut rand::rng(), 25)
412                .to_lowercase();
413            context
414                .set_config_internal(Config::StatsId, Some(&id))
415                .await?;
416            id
417        }
418    })
419}
420
421async fn get_stats_chat_id(context: &Context) -> Result<ChatId, anyhow::Error> {
422    let contact_id: ContactId = *import_vcard(context, STATISTICS_BOT_VCARD)
423        .await?
424        .first()
425        .context("Statistics bot vCard does not contain a contact")?;
426    mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
427
428    let chat_id = if let Some(res) = ChatId::lookup_by_contact(context, contact_id).await? {
429        // Already exists, no need to create.
430        res
431    } else {
432        let chat_id = ChatId::get_for_contact(context, contact_id).await?;
433        chat::set_muted(context, chat_id, MuteDuration::Forever).await?;
434        chat_id
435    };
436
437    Ok(chat_id)
438}
439
440async fn get_contact_stats(context: &Context, last_old_contact: u32) -> Result<Vec<ContactStat>> {
441    let mut verified_by_map: BTreeMap<ContactId, ContactId> = BTreeMap::new();
442    let mut bot_ids: BTreeSet<ContactId> = BTreeSet::new();
443
444    let mut contacts = context
445        .sql
446        .query_map_vec(
447            "SELECT id, fingerprint<>'', verifier, last_seen, is_bot FROM contacts c
448            WHERE id>9 AND origin>? AND addr<>?",
449            (Origin::Hidden, STATISTICS_BOT_EMAIL),
450            |row| {
451                let id = row.get(0)?;
452                let is_encrypted: bool = row.get(1)?;
453                let verifier: ContactId = row.get(2)?;
454                let last_seen: u64 = row.get(3)?;
455                let bot: bool = row.get(4)?;
456
457                let verified = match (is_encrypted, verifier) {
458                    (true, ContactId::SELF) => VerifiedStatus::Direct,
459                    (true, ContactId::UNDEFINED) => VerifiedStatus::Opportunistic,
460                    (true, _) => VerifiedStatus::Transitive, // TransitiveViaBot will be filled later
461                    (false, _) => VerifiedStatus::Unencrypted,
462                };
463
464                if verifier != ContactId::UNDEFINED {
465                    verified_by_map.insert(id, verifier);
466                }
467
468                if bot {
469                    bot_ids.insert(id);
470                }
471
472                Ok(ContactStat {
473                    id,
474                    verified,
475                    bot,
476                    direct_chat: false, // will be filled later
477                    last_seen,
478                    transitive_chain: None, // will be filled later
479                    new: id.to_u32() > last_old_contact,
480                })
481            },
482        )
483        .await?;
484
485    // Fill TransitiveViaBot and transitive_chain
486    for contact in &mut contacts {
487        if contact.verified == VerifiedStatus::Transitive {
488            let mut transitive_chain: u32 = 0;
489            let mut has_bot = false;
490            let mut current_verifier_id = contact.id;
491
492            while current_verifier_id != ContactId::SELF && transitive_chain < 100 {
493                current_verifier_id = match verified_by_map.get(&current_verifier_id) {
494                    Some(id) => *id,
495                    None => {
496                        // The chain ends here, probably because some verification was done
497                        // before we started recording verifiers.
498                        // It's unclear how long the chain really is.
499                        transitive_chain = 0;
500                        break;
501                    }
502                };
503                if bot_ids.contains(&current_verifier_id) {
504                    has_bot = true;
505                }
506                transitive_chain = transitive_chain.saturating_add(1);
507            }
508
509            if transitive_chain > 0 {
510                contact.transitive_chain = Some(transitive_chain);
511            }
512
513            if has_bot {
514                contact.verified = VerifiedStatus::TransitiveViaBot;
515            }
516        }
517    }
518
519    // Fill direct_chat
520    for contact in &mut contacts {
521        let direct_chat = context
522            .sql
523            .exists(
524                "SELECT COUNT(*)
525                FROM chats_contacts cc INNER JOIN chats
526                WHERE cc.contact_id=? AND chats.type=?",
527                (contact.id, Chattype::Single),
528            )
529            .await?;
530        contact.direct_chat = direct_chat;
531    }
532
533    Ok(contacts)
534}
535
536/// - `last_msg_id`: The last msg_id that was already counted in the previous stats.
537///   Only messages newer than that will be counted.
538/// - `one_one_chats`: If true, only messages in 1:1 chats are counted.
539///   If false, only messages in other chats (groups and broadcast channels) are counted.
540async fn get_message_stats(context: &Context) -> Result<BTreeMap<Chattype, MessageStats>> {
541    let mut map: BTreeMap<Chattype, MessageStats> = context
542        .sql
543        .query_map_collect(
544            "SELECT chattype, verified, unverified_encrypted, unencrypted, only_to_self
545            FROM stats_msgs",
546            (),
547            |row| {
548                let chattype: Chattype = row.get(0)?;
549                let verified: u32 = row.get(1)?;
550                let unverified_encrypted: u32 = row.get(2)?;
551                let unencrypted: u32 = row.get(3)?;
552                let only_to_self: u32 = row.get(4)?;
553                let message_stats = MessageStats {
554                    verified,
555                    unverified_encrypted,
556                    unencrypted,
557                    only_to_self,
558                };
559                Ok((chattype, message_stats))
560            },
561        )
562        .await?;
563
564    // Fill zeroes if a chattype wasn't present:
565    for chattype in [Chattype::Group, Chattype::Single, Chattype::OutBroadcast] {
566        map.entry(chattype).or_default();
567    }
568
569    Ok(map)
570}
571
572pub(crate) async fn update_message_stats(context: &Context) -> Result<()> {
573    for chattype in [Chattype::Single, Chattype::Group, Chattype::OutBroadcast] {
574        update_message_stats_inner(context, chattype).await?;
575    }
576    context
577        .set_config_internal(Config::StatsLastUpdate, Some(&time().to_string()))
578        .await?;
579    Ok(())
580}
581
582async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Result<()> {
583    let stats_bot_chat_id = get_stats_chat_id(context).await?;
584
585    let trans_fn = |t: &mut rusqlite::Transaction| {
586        // The ID of the last msg that was already counted in the previously sent stats.
587        // Only newer messages will be counted in the current statistics.
588        let last_counted_msg_id: u32 = t
589            .query_row(
590                "SELECT last_counted_msg_id FROM stats_msgs WHERE chattype=?",
591                (chattype,),
592                |row| row.get(0),
593            )
594            .optional()?
595            .unwrap_or(0);
596        t.execute(
597            "UPDATE stats_msgs
598            SET last_counted_msg_id=(SELECT MAX(id) FROM msgs)
599            WHERE chattype=?",
600            (chattype,),
601        )?;
602
603        // This table will hold all empty chats,
604        // i.e. all chats that do not contain any members except for self.
605        // Messages in these chats are not actually sent out.
606        t.execute(
607            "CREATE TEMP TABLE temp.empty_chats (
608                id INTEGER PRIMARY KEY
609            ) STRICT",
610            (),
611        )?;
612
613        // id>9 because chat ids 0..9 are "special" chats like the trash chat,
614        // and contact ids 0..9 are "special" contact ids like the 'device'.
615        t.execute(
616            "INSERT INTO temp.empty_chats
617            SELECT id FROM chats
618            WHERE id>9 AND NOT EXISTS(
619                SELECT *
620                FROM contacts, chats_contacts
621                WHERE chats_contacts.contact_id=contacts.id AND chats_contacts.chat_id=chats.id
622				AND contacts.id>9
623            )",
624            (),
625        )?;
626
627        // This table will hold all verified chats,
628        // i.e. all chats that only contain verified contacts.
629        t.execute(
630            "CREATE TEMP TABLE temp.verified_chats (
631                id INTEGER PRIMARY KEY
632            ) STRICT",
633            (),
634        )?;
635
636        // Verified chats are chats that are not empty,
637        // and do not contain any unverified contacts
638        t.execute(
639            "INSERT INTO temp.verified_chats
640            SELECT id FROM chats
641            WHERE id>9
642            AND id NOT IN (SELECT id FROM temp.empty_chats)
643            AND NOT EXISTS(
644                SELECT *
645                FROM contacts, chats_contacts
646                WHERE chats_contacts.contact_id=contacts.id AND chats_contacts.chat_id=chats.id
647				AND contacts.id>9
648				AND contacts.verifier=0
649            )",
650            (),
651        )?;
652
653        // This table will hold all 1:1 chats.
654        t.execute(
655            "CREATE TEMP TABLE temp.chat_with_correct_type (
656                id INTEGER PRIMARY KEY
657            ) STRICT",
658            (),
659        )?;
660
661        t.execute(
662            "INSERT INTO temp.chat_with_correct_type
663            SELECT id FROM chats
664            WHERE type=?;",
665            (chattype,),
666        )?;
667
668        // - `from_id=?` is to count only outgoing messages.
669        // - `chat_id<>?` excludes the chat with the statistics bot itself,
670        // - `id>?` excludes messages that were already counted in the previously sent statistics, or messages sent before the config was enabled
671        // - `hidden=0` excludes hidden system messages, which are not actually shown to the user.
672        //   Note that reactions are also not counted as a message.
673        // - `chat_id>9` excludes messages in the 'Trash' chat, which is an internal chat assigned to messages that are not shown to the user
674        let general_requirements = "id>? AND from_id=? AND chat_id<>?
675            AND hidden=0 AND chat_id>9 AND chat_id IN temp.chat_with_correct_type"
676            .to_string();
677        let params = (last_counted_msg_id, ContactId::SELF, stats_bot_chat_id);
678
679        let verified: u32 = t.query_row(
680            &format!(
681                "SELECT COUNT(*) FROM msgs
682                WHERE chat_id IN temp.verified_chats
683                AND {general_requirements}"
684            ),
685            params,
686            |row| row.get(0),
687        )?;
688
689        let unverified_encrypted: u32 = t.query_row(
690            &format!(
691                // (param GLOB '*\nc=1*' OR param GLOB 'c=1*') matches all messages that are end-to-end encrypted
692                "SELECT COUNT(*) FROM msgs
693                WHERE chat_id NOT IN temp.verified_chats AND chat_id NOT IN temp.empty_chats
694                AND (param GLOB '*\nc=1*' OR param GLOB 'c=1*')
695                AND {general_requirements}"
696            ),
697            params,
698            |row| row.get(0),
699        )?;
700
701        let unencrypted: u32 = t.query_row(
702            &format!(
703                "SELECT COUNT(*) FROM msgs
704                WHERE chat_id NOT IN temp.verified_chats AND chat_id NOT IN temp.empty_chats
705                AND NOT (param GLOB '*\nc=1*' OR param GLOB 'c=1*')
706                AND {general_requirements}"
707            ),
708            params,
709            |row| row.get(0),
710        )?;
711
712        let only_to_self: u32 = t.query_row(
713            &format!(
714                "SELECT COUNT(*) FROM msgs
715                WHERE chat_id IN temp.empty_chats
716                AND {general_requirements}"
717            ),
718            params,
719            |row| row.get(0),
720        )?;
721
722        t.execute("DROP TABLE temp.verified_chats", ())?;
723        t.execute("DROP TABLE temp.empty_chats", ())?;
724        t.execute("DROP TABLE temp.chat_with_correct_type", ())?;
725
726        t.execute(
727            "INSERT INTO stats_msgs(chattype) VALUES (?)
728            ON CONFLICT(chattype) DO NOTHING",
729            (chattype,),
730        )?;
731        t.execute(
732            "UPDATE stats_msgs SET
733            verified=verified+?,
734            unverified_encrypted=unverified_encrypted+?,
735            unencrypted=unencrypted+?,
736            only_to_self=only_to_self+?
737            WHERE chattype=?",
738            (
739                verified,
740                unverified_encrypted,
741                unencrypted,
742                only_to_self,
743                chattype,
744            ),
745        )?;
746
747        Ok(())
748    };
749
750    context.sql.transaction(trans_fn).await?;
751
752    Ok(())
753}
754
755pub(crate) async fn count_securejoin_ux_info(
756    context: &Context,
757    source: Option<SecurejoinSource>,
758    uipath: Option<SecurejoinUiPath>,
759) -> Result<()> {
760    if !should_send_stats(context).await? {
761        return Ok(());
762    }
763
764    let source = source
765        .context("Missing securejoin source")
766        .log_err(context)
767        .unwrap_or(SecurejoinSource::Unknown);
768
769    // We only get a UI path if the source is a QR code scan,
770    // a loaded image, or a link pasted from the QR code,
771    // so, no need to log an error if `uipath` is None:
772    let uipath = uipath.unwrap_or(SecurejoinUiPath::Unknown);
773
774    context
775        .sql
776        .transaction(|conn| {
777            conn.execute(
778                "INSERT INTO stats_securejoin_sources VALUES (?, 1)
779                ON CONFLICT (source) DO UPDATE SET count=count+1;",
780                (source.to_u32(),),
781            )?;
782
783            conn.execute(
784                "INSERT INTO stats_securejoin_uipaths VALUES (?, 1)
785                ON CONFLICT (uipath) DO UPDATE SET count=count+1;",
786                (uipath.to_u32(),),
787            )?;
788            Ok(())
789        })
790        .await?;
791
792    Ok(())
793}
794
795async fn get_securejoin_source_stats(context: &Context) -> Result<SecurejoinSources> {
796    let map: BTreeMap<SecurejoinSource, u32> = context
797        .sql
798        .query_map_collect(
799            "SELECT source, count FROM stats_securejoin_sources",
800            (),
801            |row| {
802                let source: SecurejoinSource = row.get(0)?;
803                let count: u32 = row.get(1)?;
804                Ok((source, count))
805            },
806        )
807        .await?;
808
809    let stats = SecurejoinSources {
810        unknown: *map.get(&SecurejoinSource::Unknown).unwrap_or(&0),
811        external_link: *map.get(&SecurejoinSource::ExternalLink).unwrap_or(&0),
812        internal_link: *map.get(&SecurejoinSource::InternalLink).unwrap_or(&0),
813        clipboard: *map.get(&SecurejoinSource::Clipboard).unwrap_or(&0),
814        image_loaded: *map.get(&SecurejoinSource::ImageLoaded).unwrap_or(&0),
815        scan: *map.get(&SecurejoinSource::Scan).unwrap_or(&0),
816    };
817
818    Ok(stats)
819}
820
821async fn get_securejoin_uipath_stats(context: &Context) -> Result<SecurejoinUiPaths> {
822    let map: BTreeMap<SecurejoinUiPath, u32> = context
823        .sql
824        .query_map_collect(
825            "SELECT uipath, count FROM stats_securejoin_uipaths",
826            (),
827            |row| {
828                let uipath: SecurejoinUiPath = row.get(0)?;
829                let count: u32 = row.get(1)?;
830                Ok((uipath, count))
831            },
832        )
833        .await?;
834
835    let stats = SecurejoinUiPaths {
836        other: *map.get(&SecurejoinUiPath::Unknown).unwrap_or(&0),
837        qr_icon: *map.get(&SecurejoinUiPath::QrIcon).unwrap_or(&0),
838        new_contact: *map.get(&SecurejoinUiPath::NewContact).unwrap_or(&0),
839    };
840
841    Ok(stats)
842}
843
844pub(crate) async fn count_securejoin_invite(context: &Context, invite: &QrInvite) -> Result<()> {
845    if !should_send_stats(context).await? {
846        return Ok(());
847    }
848
849    let contact = Contact::get_by_id(context, invite.contact_id()).await?;
850
851    // If the contact was created just now by the QR code scan,
852    // (or if a contact existed in the database
853    // but it was not visible in the contacts list in the UI
854    // e.g. because it's a past contact of a group we're in),
855    // then its origin is UnhandledSecurejoinQrScan.
856    let already_existed = contact.origin > Origin::UnhandledSecurejoinQrScan;
857
858    // Check whether the contact was verified already before the QR scan.
859    let already_verified = contact.is_verified(context).await?;
860
861    let typ = match invite {
862        QrInvite::Contact { .. } => "contact",
863        QrInvite::Group { .. } => "group",
864        QrInvite::Broadcast { .. } => "broadcast",
865    };
866
867    context
868        .sql
869        .execute(
870            "INSERT INTO stats_securejoin_invites (already_existed, already_verified, type)
871            VALUES (?, ?, ?)",
872            (already_existed, already_verified, typ),
873        )
874        .await?;
875
876    Ok(())
877}
878
879async fn get_securejoin_invite_stats(context: &Context) -> Result<Vec<JoinedInvite>> {
880    context
881        .sql
882        .query_map_vec(
883            "SELECT already_existed, already_verified, type FROM stats_securejoin_invites",
884            (),
885            |row| {
886                let already_existed: bool = row.get(0)?;
887                let already_verified: bool = row.get(1)?;
888                let typ: String = row.get(2)?;
889
890                Ok(JoinedInvite {
891                    already_existed,
892                    already_verified,
893                    typ,
894                })
895            },
896        )
897        .await
898}
899
900#[cfg(test)]
901mod stats_tests;