Skip to content

Commit 5f37f0a

Browse files
committed
feat: Add contact_infos, add test
1 parent 131a816 commit 5f37f0a

File tree

5 files changed

+206
-41
lines changed

5 files changed

+206
-41
lines changed

src/context.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ use std::time::Duration;
1010

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

1816
use crate::chat::{get_chat_cnt, ChatId};
@@ -25,8 +23,7 @@ use crate::contact::{Contact, ContactId};
2523
use crate::debug_logging::DebugLogging;
2624
use crate::events::{Event, EventEmitter, EventType, Events};
2725
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
28-
use crate::key::{self_fingerprint, DcKey as _};
29-
use crate::log::LogExt;
26+
use crate::key::self_fingerprint;
3027
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
3128
use crate::message::{self, MessageState, MsgId};
3229
use crate::peer_channels::Iroh;

src/context/context_tests.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ use crate::chat::{get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, M
77
use crate::chatlist::Chatlist;
88
use crate::constants::Chattype;
99
use crate::message::Message;
10-
use crate::mimeparser::SystemMessage;
1110
use crate::receive_imf::receive_imf;
12-
use crate::test_utils::{get_chat_msg, TestContext};
11+
use crate::test_utils::TestContext;
1312
use crate::tools::{create_outgoing_rfc724_mid, SystemTime};
1413

1514
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

src/receive_imf.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
3737
use crate::reaction::{set_msg_reaction, Reaction};
3838
use crate::rusqlite::OptionalExtension;
3939
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
40+
use crate::self_reporting::SELF_REPORTING_BOT_EMAIL;
4041
use crate::simplify;
4142
use crate::stock_str;
4243
use crate::sync::Sync::*;
@@ -1676,7 +1677,7 @@ async fn add_parts(
16761677
|| is_mdn
16771678
|| chat_id_blocked == Blocked::Yes
16781679
|| group_changes.silent
1679-
|| mime_parser.from.addr == "self_reporting@testrun.org"
1680+
|| mime_parser.from.addr == SELF_REPORTING_BOT_EMAIL
16801681
// No check for `hidden` because only reactions are such and they should be `InFresh`.
16811682
{
16821683
MessageState::InSeen

src/self_reporting.rs

Lines changed: 136 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
//! TODO doc comment
22
3+
use std::collections::{BTreeMap, BTreeSet};
4+
35
use anyhow::{Context as _, Result};
46
use pgp::types::PublicKeyTrait;
57
use serde::Serialize;
68

79
use crate::chat::{self, ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
810
use crate::config::Config;
9-
use crate::constants::DC_CHAT_ID_TRASH;
10-
use crate::contact::{import_vcard, mark_contact_id_as_verified, ContactId};
11+
use crate::constants::{Chattype, DC_CHAT_ID_TRASH};
12+
use crate::contact::{import_vcard, mark_contact_id_as_verified, ContactId, Origin};
1113
use crate::context::{get_version_str, Context};
1214
use crate::download::DownloadState;
1315
use crate::key::load_self_public_key;
@@ -16,6 +18,9 @@ use crate::message::{Message, Viewtype};
1618
use crate::param::{Param, Params};
1719
use crate::tools::{create_id, time};
1820

21+
pub(crate) const SELF_REPORTING_BOT_EMAIL: &str = "self_reporting@testrun.org";
22+
const SELF_REPORTING_BOT_VCARD: &str = include_str!("../assets/self-reporting-bot.vcf");
23+
1924
#[derive(Serialize)]
2025
struct Statistics {
2126
core_version: String,
@@ -25,6 +30,7 @@ struct Statistics {
2530
key_created: i64,
2631
chat_numbers: ChatNumbers,
2732
self_reporting_id: String,
33+
contact_infos: Vec<ContactInfo>,
2834
}
2935
#[derive(Default, Serialize)]
3036
struct ChatNumbers {
@@ -36,6 +42,132 @@ struct ChatNumbers {
3642
unencrypted_mua: u32,
3743
}
3844

45+
#[derive(Serialize, PartialEq)]
46+
enum VerifiedStatus {
47+
Direct,
48+
Transitive,
49+
TransitiveViaBot,
50+
Opportunistic,
51+
Unencrypted,
52+
}
53+
54+
#[derive(Serialize)]
55+
struct ContactInfo {
56+
#[serde(skip_serializing)]
57+
id: ContactId,
58+
59+
verified: VerifiedStatus,
60+
61+
#[serde(skip_serializing)]
62+
verifier: ContactId, // TODO unused, could be removed
63+
bot: bool,
64+
direct_chat: bool,
65+
last_seen: u64,
66+
//new: bool, // TODO
67+
#[serde(skip_serializing_if = "Option::is_none")]
68+
transitive_chain: Option<u32>,
69+
}
70+
71+
async fn get_contact_infos(context: &Context) -> Result<Vec<ContactInfo>> {
72+
let mut verified_by_map: BTreeMap<ContactId, ContactId> = BTreeMap::new();
73+
let mut bot_ids: BTreeSet<ContactId> = BTreeSet::new();
74+
75+
let mut contacts: Vec<ContactInfo> = context
76+
.sql
77+
.query_map(
78+
"SELECT id, fingerprint<>'', verifier, last_seen, is_bot FROM contacts c
79+
WHERE id>9 AND origin>? AND addr<>?",
80+
(Origin::Hidden, SELF_REPORTING_BOT_EMAIL),
81+
|row| {
82+
let id = row.get(0)?;
83+
let is_encrypted: bool = row.get(1)?;
84+
let verifier: ContactId = row.get(2)?;
85+
let last_seen: u64 = row.get(3)?;
86+
let bot: bool = row.get(4)?;
87+
88+
let verified = match (is_encrypted, verifier) {
89+
(true, ContactId::SELF) => VerifiedStatus::Direct,
90+
(true, ContactId::UNDEFINED) => VerifiedStatus::Opportunistic,
91+
(true, _) => VerifiedStatus::Transitive, // TransitiveViaBot will be filled later
92+
(false, _) => VerifiedStatus::Unencrypted,
93+
};
94+
95+
if verifier != ContactId::UNDEFINED {
96+
verified_by_map.insert(id, verifier);
97+
}
98+
99+
if bot {
100+
bot_ids.insert(id);
101+
}
102+
103+
Ok(ContactInfo {
104+
id,
105+
verified,
106+
verifier,
107+
bot,
108+
direct_chat: false, // will be filled later
109+
last_seen,
110+
transitive_chain: None, // will be filled later
111+
})
112+
},
113+
|rows| {
114+
rows.collect::<std::result::Result<Vec<_>, _>>()
115+
.map_err(Into::into)
116+
},
117+
)
118+
.await?;
119+
120+
// Fill TransitiveViaBot and transitive_chain
121+
for contact in contacts.iter_mut() {
122+
if contact.verified == VerifiedStatus::Transitive {
123+
let mut transitive_chain: u32 = 0;
124+
let mut has_bot = false;
125+
let mut current_verifier_id = contact.id;
126+
127+
while current_verifier_id != ContactId::SELF {
128+
current_verifier_id = match verified_by_map.get(&current_verifier_id) {
129+
Some(id) => *id,
130+
None => {
131+
// The chain ends here, probably because some verification was done
132+
// before we started recording verifiers.
133+
// It's unclear how long the chain really is.
134+
transitive_chain = 0;
135+
break;
136+
}
137+
};
138+
if bot_ids.contains(&current_verifier_id) {
139+
has_bot = true;
140+
}
141+
transitive_chain = transitive_chain.saturating_add(1);
142+
}
143+
144+
if transitive_chain > 0 {
145+
contact.transitive_chain = Some(transitive_chain);
146+
}
147+
148+
if has_bot {
149+
contact.verified = VerifiedStatus::TransitiveViaBot;
150+
}
151+
}
152+
}
153+
154+
// Fill direct_chat
155+
for contact in contacts.iter_mut() {
156+
let direct_chat = context
157+
.sql
158+
.exists(
159+
"SELECT COUNT(*)
160+
FROM chats_contacts cc INNER JOIN chats
161+
WHERE cc.contact_id=? AND chats.type=?",
162+
(contact.id, Chattype::Single),
163+
)
164+
.await?;
165+
contact.direct_chat = direct_chat;
166+
}
167+
168+
Ok(contacts)
169+
}
170+
39171
/// Sends a message with statistics about the usage of Delta Chat,
40172
/// if the last time such a message was sent
41173
/// was more than a week ago.
@@ -70,7 +202,6 @@ async fn send_self_report(context: &Context) -> Result<ChatId> {
70202
.log_err(context)
71203
.ok();
72204

73-
const SELF_REPORTING_BOT_VCARD: &str = include_str!("../assets/self-reporting-bot.vcf");
74205
let contact_id: ContactId = *import_vcard(context, SELF_REPORTING_BOT_VCARD)
75206
.await?
76207
.first()
@@ -227,40 +358,11 @@ async fn get_self_report(context: &Context) -> Result<String> {
227358
key_created,
228359
chat_numbers,
229360
self_reporting_id,
361+
contact_infos: get_contact_infos(context).await?,
230362
};
231363

232364
Ok(serde_json::to_string_pretty(&statistics)?)
233365
}
234366

235367
#[cfg(test)]
236-
mod self_reporting_tests {
237-
use anyhow::Context as _;
238-
use strum::IntoEnumIterator;
239-
use tempfile::tempdir;
240-
241-
use super::*;
242-
use crate::chat::{get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, MuteDuration};
243-
use crate::chatlist::Chatlist;
244-
use crate::constants::Chattype;
245-
use crate::mimeparser::SystemMessage;
246-
use crate::receive_imf::receive_imf;
247-
use crate::test_utils::{get_chat_msg, TestContext};
248-
use crate::tools::{create_outgoing_rfc724_mid, SystemTime};
249-
250-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
251-
async fn test_draft_self_report() -> Result<()> {
252-
let alice = TestContext::new_alice().await;
253-
254-
let chat_id = send_self_report(&alice).await?;
255-
let msg = get_chat_msg(&alice, chat_id, 0, 2).await;
256-
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
257-
258-
let chat = Chat::load_from_db(&alice, chat_id).await?;
259-
assert!(chat.is_protected());
260-
261-
let statistics_msg = get_chat_msg(&alice, chat_id, 1, 2).await;
262-
assert_eq!(statistics_msg.get_filename().unwrap(), "statistics.txt");
263-
264-
Ok(())
265-
}
266-
}
368+
mod self_reporting_tests;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use super::*;
2+
use crate::chat::Chat;
3+
use crate::mimeparser::SystemMessage;
4+
use crate::test_utils::{get_chat_msg, TestContextManager};
5+
6+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
7+
async fn test_send_self_report() -> Result<()> {
8+
let mut tcm = TestContextManager::new();
9+
let alice = &tcm.alice().await;
10+
let bob = &tcm.bob().await;
11+
12+
let chat_id = send_self_report(&alice).await?;
13+
let msg = get_chat_msg(&alice, chat_id, 0, 2).await;
14+
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
15+
16+
let chat = Chat::load_from_db(&alice, chat_id).await?;
17+
assert!(chat.is_protected());
18+
19+
let msg = get_chat_msg(&alice, chat_id, 1, 2).await;
20+
assert_eq!(msg.get_filename().unwrap(), "statistics.txt");
21+
22+
let report = tokio::fs::read(msg.get_file(&alice).unwrap()).await?;
23+
let report = std::str::from_utf8(&report)?;
24+
println!("\nEmpty account:\n{}\n", report);
25+
assert!(report.contains(r#""contact_infos": []"#));
26+
27+
let r: serde_json::Value = serde_json::from_str(&report)?;
28+
assert_eq!(
29+
r.get("contact_infos").unwrap(),
30+
&serde_json::Value::Array(vec![])
31+
);
32+
assert_eq!(r.get("core_version").unwrap(), get_version_str());
33+
34+
tcm.send_recv_accept(bob, alice, "Hi!").await;
35+
36+
let report = get_self_report(alice).await?;
37+
38+
println!("\nWith Bob:\n{report}\n");
39+
let r2: serde_json::Value = serde_json::from_str(&report)?;
40+
assert_eq!(
41+
r.get("key_created").unwrap(),
42+
r2.get("key_created").unwrap()
43+
);
44+
assert_eq!(
45+
r.get("self_reporting_id").unwrap(),
46+
r2.get("self_reporting_id").unwrap()
47+
);
48+
let contact_infos = r2.get("contact_infos").unwrap().as_array().unwrap();
49+
assert_eq!(contact_infos.len(), 1);
50+
let contact_info = &contact_infos[0];
51+
assert_eq!(
52+
contact_info.get("bot").unwrap(),
53+
&serde_json::Value::Bool(false)
54+
);
55+
assert_eq!(
56+
contact_info.get("direct_chat").unwrap(),
57+
&serde_json::Value::Bool(true)
58+
);
59+
assert!(contact_info.get("transitive_chain").is_none(),);
60+
assert_eq!(
61+
contact_info.get("verified").unwrap(),
62+
&serde_json::Value::String("Opportunistic".to_string())
63+
);
64+
65+
Ok(())
66+
}

0 commit comments

Comments
 (0)