mirror of
https://github.com/chatmail/core.git
synced 2026-05-09 01:46:30 +03:00
feat: new group membership update algorithm
New algorithm improves group consistency in cases of missing messages, restored old backups and replies from classic MUAs. Co-authored-by: Hocuri <hocuri@gmx.de> Co-authored-by: link2xt <link2xt@testrun.org>
This commit is contained in:
@@ -1641,86 +1641,136 @@ async fn apply_group_changes(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut recreate_member_list = false;
|
||||
let mut send_event_chat_modified = false;
|
||||
|
||||
let mut removed_id = None;
|
||||
let mut better_msg = None;
|
||||
let removed_id;
|
||||
if let Some(removed_addr) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupMemberRemoved)
|
||||
.cloned()
|
||||
{
|
||||
removed_id = Contact::lookup_id_by_addr(context, &removed_addr, Origin::Unknown).await?;
|
||||
recreate_member_list = true;
|
||||
match removed_id {
|
||||
Some(contact_id) => {
|
||||
better_msg = if contact_id == from_id {
|
||||
Some(stock_str::msg_group_left(context, from_id).await)
|
||||
} else {
|
||||
Some(stock_str::msg_del_member(context, &removed_addr, from_id).await)
|
||||
};
|
||||
|
||||
// True if a Delta Chat client has explicitly added our current primary address.
|
||||
let self_added =
|
||||
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
|
||||
context.get_primary_self_addr().await? == *added_addr
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Whether to allow any changes to the member list at all.
|
||||
let allow_member_list_changes =
|
||||
if chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await? || self_added {
|
||||
// Reject old group changes.
|
||||
chat_id
|
||||
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
|
||||
.await?
|
||||
} else {
|
||||
// Member list changes are not allowed if we're not in the group
|
||||
// and are not explicitly added.
|
||||
// This message comes from a Delta Chat that restored an old backup
|
||||
// or the message is a MUA reply to an old message.
|
||||
false
|
||||
};
|
||||
|
||||
// Whether to rebuild the member list from scratch.
|
||||
let recreate_member_list = if allow_member_list_changes {
|
||||
// Recreate member list if the message comes from a MUA as these messages do _not_ set add/remove headers.
|
||||
// Always recreate membership list if self has been added.
|
||||
if !mime_parser.has_chat_version() || self_added {
|
||||
true
|
||||
} else {
|
||||
match mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
// If we don't know the referenced message, we missed some messages.
|
||||
// Maybe they added/removed members, so we need to recreate our member list.
|
||||
Some(reply_to) => rfc724_mid_exists(context, reply_to).await?.is_none(),
|
||||
None => false,
|
||||
}
|
||||
None => warn!(context, "Removed {removed_addr:?} has no contact_id."),
|
||||
}
|
||||
} else {
|
||||
removed_id = None;
|
||||
if let Some(added_member) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupMemberAdded)
|
||||
.cloned()
|
||||
{
|
||||
better_msg = Some(stock_str::msg_add_member(context, &added_member, from_id).await);
|
||||
recreate_member_list = true;
|
||||
} else if let Some(old_name) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||
// See create_or_lookup_group() for explanation
|
||||
.map(|s| s.trim())
|
||||
{
|
||||
if let Some(grpname) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupName)
|
||||
// See create_or_lookup_group() for explanation
|
||||
.map(|grpname| grpname.trim())
|
||||
.filter(|grpname| grpname.len() < 200)
|
||||
{
|
||||
if chat_id
|
||||
.update_timestamp(context, Param::GroupNameTimestamp, sent_timestamp)
|
||||
.await?
|
||||
{
|
||||
info!(context, "Updating grpname for chat {chat_id}.");
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET name=? WHERE id=?;",
|
||||
(strip_rtlo_characters(grpname), chat_id),
|
||||
)
|
||||
.await?;
|
||||
false
|
||||
};
|
||||
|
||||
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
|
||||
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
|
||||
if let Some(contact_id) = removed_id {
|
||||
if allow_member_list_changes {
|
||||
// Remove a single member from the chat.
|
||||
if !recreate_member_list {
|
||||
chat::remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
|
||||
better_msg =
|
||||
Some(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
|
||||
better_msg = if contact_id == from_id {
|
||||
Some(stock_str::msg_group_left(context, from_id).await)
|
||||
} else {
|
||||
Some(stock_str::msg_del_member(context, removed_addr, from_id).await)
|
||||
};
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Ignoring removal of {removed_addr:?} from {chat_id}."
|
||||
);
|
||||
}
|
||||
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
|
||||
if value == "group-avatar-changed" {
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
// this is just an explicit message containing the group-avatar,
|
||||
// apart from that, the group-avatar is send along with various other messages
|
||||
better_msg = match avatar_action {
|
||||
AvatarAction::Delete => {
|
||||
Some(stock_str::msg_grp_img_deleted(context, from_id).await)
|
||||
}
|
||||
AvatarAction::Change(_) => {
|
||||
Some(stock_str::msg_grp_img_changed(context, from_id).await)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
warn!(context, "Removed {removed_addr:?} has no contact id.")
|
||||
}
|
||||
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
|
||||
if allow_member_list_changes {
|
||||
// Add a single member to the chat.
|
||||
if !recreate_member_list {
|
||||
if let Some(contact_id) =
|
||||
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
|
||||
{
|
||||
chat::add_to_chat_contacts_table(context, chat_id, &[contact_id]).await?;
|
||||
send_event_chat_modified = true;
|
||||
} else {
|
||||
warn!(context, "Added {added_addr:?} has no contact id.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mime_parser.has_chat_version() {
|
||||
// If a classical MUA user adds someone to TO/CC, then the DC user shall
|
||||
// see this addition and have the new recipient in the member list.
|
||||
recreate_member_list = true;
|
||||
better_msg = Some(stock_str::msg_add_member(context, added_addr, from_id).await);
|
||||
} else {
|
||||
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
|
||||
}
|
||||
} else if let Some(old_name) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||
// See create_or_lookup_group() for explanation
|
||||
.map(|s| s.trim())
|
||||
{
|
||||
if let Some(grpname) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupName)
|
||||
// See create_or_lookup_group() for explanation
|
||||
.map(|grpname| grpname.trim())
|
||||
.filter(|grpname| grpname.len() < 200)
|
||||
{
|
||||
if chat_id
|
||||
.update_timestamp(context, Param::GroupNameTimestamp, sent_timestamp)
|
||||
.await?
|
||||
{
|
||||
info!(context, "Updating grpname for chat {chat_id}.");
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET name=? WHERE id=?;",
|
||||
(strip_rtlo_characters(grpname), chat_id),
|
||||
)
|
||||
.await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
|
||||
better_msg = Some(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
|
||||
}
|
||||
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
|
||||
if value == "group-avatar-changed" {
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
// this is just an explicit message containing the group-avatar,
|
||||
// apart from that, the group-avatar is send along with various other messages
|
||||
better_msg = match avatar_action {
|
||||
AvatarAction::Delete => {
|
||||
Some(stock_str::msg_grp_img_deleted(context, from_id).await)
|
||||
}
|
||||
AvatarAction::Change(_) => {
|
||||
Some(stock_str::msg_grp_img_changed(context, from_id).await)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
|
||||
@@ -1734,49 +1784,46 @@ async fn apply_group_changes(
|
||||
chat_id
|
||||
.inner_set_protection(context, ProtectionStatus::Protected)
|
||||
.await?;
|
||||
recreate_member_list = true;
|
||||
}
|
||||
}
|
||||
|
||||
// add members to group/check members
|
||||
// Recreate the member list.
|
||||
if recreate_member_list {
|
||||
if chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await?
|
||||
&& !chat::is_contact_in_chat(context, chat_id, from_id).await?
|
||||
{
|
||||
if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
|
||||
warn!(
|
||||
context,
|
||||
"Contact {from_id} attempts to modify group chat {chat_id} member list without being a member."
|
||||
);
|
||||
} else if chat_id
|
||||
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
|
||||
.await?
|
||||
{
|
||||
let mut members_to_add = vec![];
|
||||
if removed_id.is_some()
|
||||
|| !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await?
|
||||
{
|
||||
// Members could have been removed while we were
|
||||
// absent. We can't use existing member list and need to
|
||||
// start from scratch.
|
||||
} else {
|
||||
// Only delete old contacts if the sender is not a classical MUA user:
|
||||
// Classical MUA users usually don't intend to remove users from an email
|
||||
// thread, so if they removed a recipient then it was probably by accident.
|
||||
if mime_parser.has_chat_version() {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,))
|
||||
.await?;
|
||||
|
||||
members_to_add.push(ContactId::SELF);
|
||||
}
|
||||
|
||||
let mut members_to_add = HashSet::new();
|
||||
members_to_add.extend(to_ids);
|
||||
members_to_add.insert(ContactId::SELF);
|
||||
|
||||
if !from_id.is_special() {
|
||||
members_to_add.push(from_id);
|
||||
members_to_add.insert(from_id);
|
||||
}
|
||||
members_to_add.extend(to_ids);
|
||||
if let Some(removed_id) = removed_id {
|
||||
members_to_add.retain(|id| *id != removed_id);
|
||||
}
|
||||
members_to_add.dedup();
|
||||
|
||||
info!(context, "Adding {members_to_add:?} to chat id={chat_id}.");
|
||||
chat::add_to_chat_contacts_table(context, chat_id, &members_to_add).await?;
|
||||
if let Some(removed_id) = removed_id {
|
||||
members_to_add.remove(&removed_id);
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Recreating chat {chat_id} with members {members_to_add:?}."
|
||||
);
|
||||
|
||||
chat::add_to_chat_contacts_table(context, chat_id, &Vec::from_iter(members_to_add))
|
||||
.await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user