Don't send group update messages when member labels are changed.

This commit is contained in:
jeffrey-signal
2026-02-03 12:20:33 -05:00
committed by GitHub
parent 0cd93986bd
commit ff726ec4d2
8 changed files with 189 additions and 148 deletions

View File

@@ -3,15 +3,20 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
@file:JvmName("DecryptedGroupExtensions")
package org.whispersystems.signalservice.api.groupsv2
import org.signal.core.models.ServiceId
import org.signal.core.models.ServiceId.ACI
import org.signal.storageservice.storage.protos.groups.AccessControl
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroupChange
import org.signal.storageservice.storage.protos.groups.local.DecryptedMember
import org.signal.storageservice.storage.protos.groups.local.DecryptedModifyMemberLabel
import org.signal.storageservice.storage.protos.groups.local.DecryptedPendingMember
import org.signal.storageservice.storage.protos.groups.local.DecryptedRequestingMember
import org.signal.storageservice.storage.protos.groups.local.EnabledState
import java.util.Optional
fun Collection<DecryptedMember>.toAciListWithUnknowns(): List<ACI> {
@@ -54,3 +59,78 @@ fun DecryptedGroup.Builder.setModifyMemberLabelActions(
members = updatedMembers
}
/**
* Returns the group change fields that contain actual changes (value is not empty or default).
*/
fun DecryptedGroupChange.getChangedFields(): Set<GroupChangeField> {
return buildSet {
if (newIsAnnouncementGroup != EnabledState.UNKNOWN) add(GroupChangeField.ANNOUNCEMENT_GROUP)
if (newAttributeAccess != AccessControl.AccessRequired.UNKNOWN) add(GroupChangeField.ATTRIBUTE_ACCESS)
if (newAvatar != null) add(GroupChangeField.AVATAR)
if (deleteBannedMembers.isNotEmpty()) add(GroupChangeField.BANNED_MEMBER_REMOVALS)
if (newBannedMembers.isNotEmpty()) add(GroupChangeField.BANNED_MEMBERS)
if (newDescription != null) add(GroupChangeField.DESCRIPTION)
if (newInviteLinkAccess != AccessControl.AccessRequired.UNKNOWN) add(GroupChangeField.INVITE_LINK_ACCESS)
if (newInviteLinkPassword.size != 0) add(GroupChangeField.INVITE_LINK_PASSWORD)
if (newMemberAccess != AccessControl.AccessRequired.UNKNOWN) add(GroupChangeField.MEMBER_ACCESS)
if (modifyMemberLabels.isNotEmpty()) add(GroupChangeField.MEMBER_LABELS)
if (deleteMembers.isNotEmpty()) add(GroupChangeField.MEMBER_REMOVALS)
if (modifyMemberRoles.isNotEmpty()) add(GroupChangeField.MEMBER_ROLES)
if (newMembers.isNotEmpty()) add(GroupChangeField.MEMBERS)
if (promotePendingMembers.isNotEmpty()) add(GroupChangeField.PENDING_MEMBER_PROMOTIONS)
if (deletePendingMembers.isNotEmpty()) add(GroupChangeField.PENDING_MEMBER_REMOVALS)
if (newPendingMembers.isNotEmpty()) add(GroupChangeField.PENDING_MEMBERS)
if (promotePendingPniAciMembers.isNotEmpty()) add(GroupChangeField.PNI_ACI_PROMOTIONS)
if (modifiedProfileKeys.isNotEmpty()) add(GroupChangeField.PROFILE_KEYS)
if (promoteRequestingMembers.isNotEmpty()) add(GroupChangeField.REQUESTING_MEMBER_APPROVALS)
if (deleteRequestingMembers.isNotEmpty()) add(GroupChangeField.REQUESTING_MEMBER_REMOVALS)
if (newRequestingMembers.isNotEmpty()) add(GroupChangeField.REQUESTING_MEMBERS)
if (newTimer != null) add(GroupChangeField.TIMER)
if (newTitle != null) add(GroupChangeField.TITLE)
}
}
/**
* Returns true if the group change should not be announced to the group members.
*/
@JvmOverloads
fun DecryptedGroupChange.isSilent(
changedFields: Set<GroupChangeField> = getChangedFields()
): Boolean {
return GroupChangeField.silentChanges.containsAll(changedFields)
}
/**
* Fields representing possible changes to a group state.
* To add a new field, update the enum and add corresponding checks in getChangedFields().
*/
enum class GroupChangeField(val changeSilently: Boolean = false) {
ANNOUNCEMENT_GROUP,
ATTRIBUTE_ACCESS,
AVATAR,
BANNED_MEMBER_REMOVALS,
BANNED_MEMBERS(changeSilently = true),
DESCRIPTION,
INVITE_LINK_ACCESS,
INVITE_LINK_PASSWORD,
MEMBER_ACCESS,
MEMBER_LABELS(changeSilently = true),
MEMBER_REMOVALS,
MEMBER_ROLES,
MEMBERS,
PENDING_MEMBER_PROMOTIONS,
PENDING_MEMBER_REMOVALS,
PENDING_MEMBERS,
PNI_ACI_PROMOTIONS,
PROFILE_KEYS(changeSilently = true),
REQUESTING_MEMBER_APPROVALS,
REQUESTING_MEMBER_REMOVALS,
REQUESTING_MEMBERS,
TIMER,
TITLE;
companion object {
val silentChanges = GroupChangeField.entries.filter { it.changeSilently }
}
}

View File

@@ -8,7 +8,6 @@ import org.signal.storageservice.storage.protos.groups.local.DecryptedBannedMemb
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.storage.protos.groups.local.DecryptedMember;
import org.signal.storageservice.storage.protos.groups.local.DecryptedModifyMemberLabel;
import org.signal.storageservice.storage.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.storage.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.storage.protos.groups.local.DecryptedPendingMemberRemoval;
@@ -338,7 +337,7 @@ public final class DecryptedGroupUtil {
applyPromotePendingPniAciMemberActions(builder, change.promotePendingPniAciMembers);
DecryptedGroupExtensionsKt.setModifyMemberLabelActions(builder, change.modifyMemberLabels);
DecryptedGroupExtensions.setModifyMemberLabelActions(builder, change.modifyMemberLabels);
return builder.build();
}
@@ -722,73 +721,4 @@ public final class DecryptedGroupUtil {
}
return -1;
}
public static boolean changeIsEmpty(DecryptedGroupChange change) {
return change.modifiedProfileKeys.size() == 0 && // field 6
changeIsEmptyExceptForProfileKeyChanges(change);
}
/*
* When updating this, update {@link #changeIsEmptyExceptForBanChangesAndOptionalProfileKeyChanges(DecryptedGroupChange)}
*/
public static boolean changeIsEmptyExceptForProfileKeyChanges(DecryptedGroupChange change) {
return change.newMembers.size() == 0 && // field 3
change.deleteMembers.size() == 0 && // field 4
change.modifyMemberRoles.size() == 0 && // field 5
change.newPendingMembers.size() == 0 && // field 7
change.deletePendingMembers.size() == 0 && // field 8
change.promotePendingMembers.size() == 0 && // field 9
change.newTitle == null && // field 10
change.newAvatar == null && // field 11
change.newTimer == null && // field 12
isEmpty(change.newAttributeAccess) && // field 13
isEmpty(change.newMemberAccess) && // field 14
isEmpty(change.newInviteLinkAccess) && // field 15
change.newRequestingMembers.size() == 0 && // field 16
change.deleteRequestingMembers.size() == 0 && // field 17
change.promoteRequestingMembers.size() == 0 && // field 18
change.newInviteLinkPassword.size() == 0 && // field 19
change.newDescription == null && // field 20
isEmpty(change.newIsAnnouncementGroup) && // field 21
change.newBannedMembers.size() == 0 && // field 22
change.deleteBannedMembers.size() == 0 && // field 23
change.promotePendingPniAciMembers.size() == 0 && // field 24
change.modifyMemberLabels.isEmpty(); // field 26
}
public static boolean changeIsEmptyExceptForBanChangesAndOptionalProfileKeyChanges(DecryptedGroupChange change) {
return (change.newBannedMembers.size() != 0 || change.deleteBannedMembers.size() != 0) &&
change.newMembers.size() == 0 && // field 3
change.deleteMembers.size() == 0 && // field 4
change.modifyMemberRoles.size() == 0 && // field 5
change.newPendingMembers.size() == 0 && // field 7
change.deletePendingMembers.size() == 0 && // field 8
change.promotePendingMembers.size() == 0 && // field 9
change.newTitle == null && // field 10
change.newAvatar == null && // field 11
change.newTimer == null && // field 12
isEmpty(change.newAttributeAccess) && // field 13
isEmpty(change.newMemberAccess) && // field 14
isEmpty(change.newInviteLinkAccess) && // field 15
change.newRequestingMembers.size() == 0 && // field 16
change.deleteRequestingMembers.size() == 0 && // field 17
change.promoteRequestingMembers.size() == 0 && // field 18
change.newInviteLinkPassword.size() == 0 && // field 19
change.newDescription == null && // field 20
isEmpty(change.newIsAnnouncementGroup) && // field 21
change.promotePendingPniAciMembers.size() == 0 && // field 24
change.modifyMemberLabels.isEmpty(); // field 26
}
static boolean isEmpty(AccessControl.AccessRequired newAttributeAccess) {
return newAttributeAccess == AccessControl.AccessRequired.UNKNOWN;
}
static boolean isEmpty(EnabledState enabledState) {
return enabledState == EnabledState.UNKNOWN;
}
public static boolean changeIsSilent(DecryptedGroupChange plainGroupChange) {
return changeIsEmptyExceptForProfileKeyChanges(plainGroupChange) || changeIsEmptyExceptForBanChangesAndOptionalProfileKeyChanges(plainGroupChange);
}
}