Skip to content

Commit de63527

Browse files
committed
feat: new group consistency algorithm
This implements new group consistency algorithm described in <#6401> New `Chat-Group-Member-Timestamps` header is added to send timestamps of member additions and removals. Member is part of the chat if its addition timestamp is greater or equal to the removal timestamp.
1 parent cb43382 commit de63527

17 files changed

+750
-486
lines changed

src/chat.rs

Lines changed: 179 additions & 87 deletions
Large diffs are not rendered by default.

src/chatlist.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ impl Chatlist {
144144
ORDER BY timestamp DESC, id DESC LIMIT 1)
145145
WHERE c.id>9
146146
AND c.blocked!=1
147-
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
147+
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2 AND add_timestamp >= remove_timestamp)
148148
GROUP BY c.id
149149
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
150150
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
@@ -261,7 +261,7 @@ impl Chatlist {
261261
WHERE c.id>9 AND c.id!=?
262262
AND c.blocked=0
263263
AND NOT c.archived=?
264-
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?))
264+
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=? AND add_timestamp >= remove_timestamp))
265265
GROUP BY c.id
266266
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
267267
(

src/contact.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ impl ContactId {
114114
SET gossiped_timestamp=0
115115
WHERE EXISTS (SELECT 1 FROM chats_contacts
116116
WHERE chats_contacts.chat_id=chats.id
117-
AND chats_contacts.contact_id=?)",
117+
AND chats_contacts.contact_id=?
118+
AND chats_contacts.add_timestamp >= chats_contacts.remove_timestamp)",
118119
(self,),
119120
)
120121
.await?;

src/headerdef.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ pub enum HeaderDef {
6565
ChatGroupMemberAdded,
6666
ChatContent,
6767

68+
/// Past members of the group.
69+
ChatGroupPastMembers,
70+
71+
/// Space-separated timestamps of member addition
72+
/// for members listed in the `To` field
73+
/// followed by timestamps of member removal
74+
/// for members listed in the `Chat-Group-Past-Members` field.
75+
ChatGroupMemberTimestamps,
76+
6877
/// Duration of the attached media file.
6978
ChatDuration,
7079

src/mimefactory.rs

Lines changed: 170 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,36 @@ pub struct MimeFactory {
6666

6767
selfstatus: String,
6868

69-
/// Vector of pairs of recipient name and address
70-
recipients: Vec<(String, String)>,
69+
/// Vector of actual recipient addresses.
70+
///
71+
/// This is the list of addresses the message should be sent to.
72+
/// It is not the same as the `To` header,
73+
/// because in case of "member removed" message
74+
/// removed member is in the recipient list,
75+
/// but not in the `To` header.
76+
/// In case of broadcast lists there are multiple recipients,
77+
/// but the `To` header has no members.
78+
///
79+
/// If `bcc_self` configuration is enabled,
80+
/// this list will be extended with own address later,
81+
/// but `MimeFactory` is not responsible for this.
82+
recipients: Vec<String>,
83+
84+
/// Vector of pairs of recipient name and address that goes into the `To` field.
85+
///
86+
/// The list of actual message recipient addresses may be different,
87+
/// e.g. if members are hidden for broadcast lists.
88+
to: Vec<(String, String)>,
89+
90+
/// Vector of pairs of past group member names and addresses.
91+
past_members: Vec<(String, String)>,
92+
93+
/// Timestamps of the members in the same order as in the `recipients`
94+
/// followed by `past_members`.
95+
///
96+
/// If this is not empty, its length
97+
/// should be the sum of `recipients` and `past_members` length.
98+
member_timestamps: Vec<i64>,
7199

72100
timestamp: i64,
73101
loaded: Loaded,
@@ -128,6 +156,7 @@ impl MimeFactory {
128156
pub async fn from_msg(context: &Context, msg: Message) -> Result<MimeFactory> {
129157
let chat = Chat::load_from_db(context, msg.chat_id).await?;
130158
let attach_profile_data = Self::should_attach_profile_data(&msg);
159+
let undisclosed_recipients = chat.typ == Chattype::Broadcast;
131160

132161
let from_addr = context.get_primary_self_addr().await?;
133162
let config_displayname = context
@@ -145,47 +174,101 @@ impl MimeFactory {
145174
(name, None)
146175
};
147176

148-
let mut recipients = Vec::with_capacity(5);
177+
let mut recipients = Vec::new();
178+
let mut to = Vec::new();
179+
let mut past_members = Vec::new();
180+
let mut member_timestamps = Vec::new();
149181
let mut recipient_ids = HashSet::new();
150182
let mut req_mdn = false;
151183

152184
if chat.is_self_talk() {
153185
if msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
154-
recipients.push((from_displayname.to_string(), from_addr.to_string()));
186+
recipients.push(from_addr.to_string());
187+
to.push((from_displayname.to_string(), from_addr.to_string()));
155188
}
156189
} else if chat.is_mailing_list() {
157190
let list_post = chat
158191
.param
159192
.get(Param::ListPost)
160193
.context("Can't write to mailinglist without ListPost param")?;
161-
recipients.push(("".to_string(), list_post.to_string()));
194+
to.push(("".to_string(), list_post.to_string()));
195+
recipients.push(list_post.to_string());
162196
} else {
197+
let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
198+
msg.param.get(Param::Arg)
199+
} else {
200+
None
201+
};
202+
163203
context
164204
.sql
165205
.query_map(
166-
"SELECT c.authname, c.addr, c.id \
167-
FROM chats_contacts cc \
168-
LEFT JOIN contacts c ON cc.contact_id=c.id \
169-
WHERE cc.chat_id=? AND cc.contact_id>9;",
170-
(msg.chat_id,),
206+
"SELECT c.authname, c.addr, c.id, cc.add_timestamp, cc.remove_timestamp
207+
FROM chats_contacts cc
208+
LEFT JOIN contacts c ON cc.contact_id=c.id
209+
WHERE cc.chat_id=? AND cc.contact_id>9 OR (cc.contact_id=1 AND ?)",
210+
(msg.chat_id, chat.typ == Chattype::Group),
171211
|row| {
172212
let authname: String = row.get(0)?;
173213
let addr: String = row.get(1)?;
174214
let id: ContactId = row.get(2)?;
175-
Ok((authname, addr, id))
215+
let add_timestamp: i64 = row.get(3)?;
216+
let remove_timestamp: i64 = row.get(4)?;
217+
Ok((authname, addr, id, add_timestamp, remove_timestamp))
176218
},
177219
|rows| {
220+
let mut past_member_timestamps = Vec::new();
221+
178222
for row in rows {
179-
let (authname, addr, id) = row?;
180-
if !recipients_contain_addr(&recipients, &addr) {
181-
let name = match attach_profile_data {
182-
true => authname,
183-
false => "".to_string(),
184-
};
185-
recipients.push((name, addr));
223+
let (authname, addr, id, add_timestamp, remove_timestamp) = row?;
224+
let addr = if id == ContactId::SELF {
225+
from_addr.to_string()
226+
} else {
227+
addr
228+
};
229+
let name = match attach_profile_data {
230+
true => authname,
231+
false => "".to_string(),
232+
};
233+
if add_timestamp >= remove_timestamp {
234+
if !recipients_contain_addr(&to, &addr) {
235+
recipients.push(addr.clone());
236+
if !undisclosed_recipients {
237+
to.push((name, addr));
238+
member_timestamps.push(add_timestamp);
239+
}
240+
}
241+
recipient_ids.insert(id);
242+
} else {
243+
// Row is a tombstone,
244+
// member is not actually part of the group.
245+
if !recipients_contain_addr(&past_members, &addr) {
246+
if let Some(email_to_remove) = email_to_remove {
247+
if email_to_remove == addr {
248+
// This is a "member removed" message,
249+
// we need to notify removed member
250+
// that it was removed.
251+
recipients.push(addr.clone());
252+
}
253+
}
254+
if !undisclosed_recipients {
255+
past_members.push((name, addr));
256+
past_member_timestamps.push(remove_timestamp);
257+
}
258+
}
186259
}
187-
recipient_ids.insert(id);
188260
}
261+
262+
debug_assert!(member_timestamps.len() >= to.len());
263+
264+
if to.len() > 1 {
265+
if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
266+
to.remove(position);
267+
member_timestamps.remove(position);
268+
}
269+
}
270+
271+
member_timestamps.extend(past_member_timestamps);
189272
Ok(())
190273
},
191274
)
@@ -226,12 +309,19 @@ impl MimeFactory {
226309
};
227310
let attach_selfavatar = Self::should_attach_selfavatar(context, &msg).await;
228311

312+
debug_assert!(
313+
member_timestamps.is_empty()
314+
|| to.len() + past_members.len() == member_timestamps.len()
315+
);
229316
let factory = MimeFactory {
230317
from_addr,
231318
from_displayname,
232319
sender_displayname,
233320
selfstatus,
234321
recipients,
322+
to,
323+
past_members,
324+
member_timestamps,
235325
timestamp: msg.timestamp_sort,
236326
loaded: Loaded::Message { msg, chat },
237327
in_reply_to,
@@ -259,7 +349,10 @@ impl MimeFactory {
259349
from_displayname: "".to_string(),
260350
sender_displayname: None,
261351
selfstatus: "".to_string(),
262-
recipients: vec![("".to_string(), contact.get_addr().to_string())],
352+
recipients: vec![contact.get_addr().to_string()],
353+
to: vec![("".to_string(), contact.get_addr().to_string())],
354+
past_members: vec![],
355+
member_timestamps: vec![],
263356
timestamp,
264357
loaded: Loaded::Mdn {
265358
rfc724_mid,
@@ -283,11 +376,7 @@ impl MimeFactory {
283376
let self_addr = context.get_primary_self_addr().await?;
284377

285378
let mut res = Vec::new();
286-
for (_, addr) in self
287-
.recipients
288-
.iter()
289-
.filter(|(_, addr)| addr != &self_addr)
290-
{
379+
for addr in self.recipients.iter().filter(|&addr| *addr != self_addr) {
291380
res.push((Peerstate::from_addr(context, addr).await?, addr.clone()));
292381
}
293382

@@ -475,10 +564,7 @@ impl MimeFactory {
475564
}
476565

477566
pub fn recipients(&self) -> Vec<String> {
478-
self.recipients
479-
.iter()
480-
.map(|(_, addr)| addr.clone())
481-
.collect()
567+
self.recipients.clone()
482568
}
483569

484570
/// Consumes a `MimeFactory` and renders it into a message which is then stored in
@@ -488,46 +574,33 @@ impl MimeFactory {
488574

489575
let from = new_address_with_name(&self.from_displayname, self.from_addr.clone());
490576

491-
let undisclosed_recipients = match &self.loaded {
492-
Loaded::Message { chat, .. } => chat.typ == Chattype::Broadcast,
493-
Loaded::Mdn { .. } => false,
494-
};
495-
496577
let mut to = Vec::new();
497-
if undisclosed_recipients {
578+
for (name, addr) in &self.to {
579+
if name.is_empty() {
580+
to.push(Address::new_mailbox(addr.clone()));
581+
} else {
582+
to.push(new_address_with_name(name, addr.clone()));
583+
}
584+
}
585+
586+
let mut past_members = Vec::new(); // Contents of `Chat-Group-Past-Members` header.
587+
for (name, addr) in &self.past_members {
588+
if name.is_empty() {
589+
past_members.push(Address::new_mailbox(addr.clone()));
590+
} else {
591+
past_members.push(new_address_with_name(name, addr.clone()));
592+
}
593+
}
594+
595+
debug_assert!(
596+
self.member_timestamps.is_empty()
597+
|| to.len() + past_members.len() == self.member_timestamps.len()
598+
);
599+
if to.is_empty() {
498600
to.push(Address::new_group(
499601
"hidden-recipients".to_string(),
500602
Vec::new(),
501603
));
502-
} else {
503-
let email_to_remove = match &self.loaded {
504-
Loaded::Message { msg, .. } => {
505-
if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
506-
msg.param.get(Param::Arg)
507-
} else {
508-
None
509-
}
510-
}
511-
Loaded::Mdn { .. } => None,
512-
};
513-
514-
for (name, addr) in &self.recipients {
515-
if let Some(email_to_remove) = email_to_remove {
516-
if email_to_remove == addr {
517-
continue;
518-
}
519-
}
520-
521-
if name.is_empty() {
522-
to.push(Address::new_mailbox(addr.clone()));
523-
} else {
524-
to.push(new_address_with_name(name, addr.clone()));
525-
}
526-
}
527-
528-
if to.is_empty() {
529-
to.push(from.clone());
530-
}
531604
}
532605

533606
// Start with Internet Message Format headers in the order of the standard example
@@ -540,6 +613,26 @@ impl MimeFactory {
540613
headers.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
541614
}
542615
headers.push(Header::new_with_value("To".into(), to.clone()).unwrap());
616+
if !past_members.is_empty() {
617+
headers.push(
618+
Header::new_with_value("Chat-Group-Past-Members".into(), past_members.clone())
619+
.unwrap(),
620+
);
621+
}
622+
623+
if !self.member_timestamps.is_empty() {
624+
headers.push(
625+
Header::new_with_value(
626+
"Chat-Group-Member-Timestamps".into(),
627+
self.member_timestamps
628+
.iter()
629+
.map(|ts| ts.to_string())
630+
.collect::<Vec<String>>()
631+
.join(" "),
632+
)
633+
.unwrap(),
634+
);
635+
}
543636

544637
let subject_str = self.subject_str(context).await?;
545638
let encoded_subject = if subject_str
@@ -2461,8 +2554,9 @@ mod tests {
24612554
// Alice creates a group with Bob and Claire and then removes Bob.
24622555
let alice = TestContext::new_alice().await;
24632556

2557+
let claire_addr = "[email protected]";
24642558
let bob_id = Contact::create(&alice, "Bob", "[email protected]").await?;
2465-
let claire_id = Contact::create(&alice, "Claire", "[email protected]").await?;
2559+
let claire_id = Contact::create(&alice, "Claire", claire_addr).await?;
24662560

24672561
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
24682562
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
@@ -2478,10 +2572,17 @@ mod tests {
24782572
.get_first_header("To")
24792573
.context("no To: header parsed")?;
24802574
let to = addrparse_header(to)?;
2481-
let mailbox = to
2482-
.extract_single_info()
2483-
.context("to: field does not contain exactly one address")?;
2484-
assert_eq!(mailbox.addr, "[email protected]");
2575+
for to_addr in to.iter() {
2576+
match to_addr {
2577+
mailparse::MailAddr::Single(ref info) => {
2578+
// Addresses should be of existing members (Alice and Bob) and not Claire.
2579+
assert_ne!(info.addr, claire_addr);
2580+
}
2581+
mailparse::MailAddr::Group(_) => {
2582+
panic!("Group addresses are not expected here");
2583+
}
2584+
}
2585+
}
24852586

24862587
Ok(())
24872588
}

0 commit comments

Comments
 (0)