Show group update messages for member label permission changes.

This commit is contained in:
jeffrey-signal
2026-03-09 10:18:12 -04:00
parent 54aa477b28
commit 6100664287
9 changed files with 106 additions and 8 deletions

View File

@@ -15,10 +15,10 @@ import org.signal.benchmark.setup.Generator
import org.signal.benchmark.setup.Harness
import org.signal.benchmark.setup.OtherClient
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.TestDbUtils
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.internal.push.Envelope

View File

@@ -13,7 +13,6 @@ import kotlinx.coroutines.launch
import org.signal.benchmark.setup.Harness
import org.signal.benchmark.setup.TestMessages
import org.signal.benchmark.setup.TestUsers
import org.signal.core.models.ServiceId.PNI
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.backup.v2.BackupRepository

View File

@@ -43,7 +43,6 @@ import org.signal.core.util.delete
import org.signal.core.util.deleteAll
import org.signal.core.util.exists
import org.signal.core.util.forEach
import org.signal.core.util.forceForeignKeyConstraintsEnabled
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
import org.signal.core.util.readToList

View File

@@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.GroupJoinRequestUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberAddedUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberJoinedByLinkUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberJoinedUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberLabelAccessLevelChangeUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberLeftUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberRemovedUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupMembershipAccessLevelChangeUpdate
@@ -146,6 +147,7 @@ object GroupsV2UpdateMessageConverter {
translateNewTimer(change, editorUnknown, updates)
translateNewAttributeAccess(change, editorUnknown, updates)
translateNewMembershipAccess(change, editorUnknown, updates)
translateNewMemberLabelAccess(change, editorUnknown, updates)
translateNewGroupInviteLinkAccess(previousGroupState, change, editorUnknown, updates)
translateRequestingMembers(selfIds, change, editorUnknown, updates)
translateRequestingMemberApprovals(selfIds, change, editorUnknown, updates)
@@ -437,6 +439,21 @@ object GroupsV2UpdateMessageConverter {
}
}
@JvmStatic
fun translateNewMemberLabelAccess(change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
if (change.newMemberLabelAccess !== AccessRequired.UNKNOWN) {
val editorAci = if (editorUnknown) null else change.editorServiceIdBytes
updates.add(
GroupChangeChatUpdate.Update(
groupMemberLabelAccessLevelChangeUpdate = GroupMemberLabelAccessLevelChangeUpdate(
updaterAci = editorAci,
accessLevel = translateGv2AccessLevel(change.newMemberLabelAccess)
)
)
)
}
}
@JvmStatic
fun translateNewGroupInviteLinkAccess(previousGroupState: DecryptedGroup?, change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
var previousAccessControl: AccessRequired? = null

View File

@@ -11,7 +11,9 @@ import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.ContextCompat;
import org.signal.core.models.ServiceId;
import org.signal.core.util.BidiUtil;
import org.signal.core.util.UuidUtil;
import org.signal.storageservice.storage.protos.groups.AccessControl;
import org.signal.storageservice.storage.protos.groups.Member;
import org.signal.storageservice.storage.protos.groups.local.DecryptedApproveMember;
@@ -46,6 +48,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.GroupJoinRequestUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberAddedUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberJoinedByLinkUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberJoinedUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberLabelAccessLevelChangeUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberLeftUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberRemovedUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupMembershipAccessLevelChangeUpdate;
@@ -66,9 +69,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.signal.core.models.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceIds;
import org.signal.core.util.UuidUtil;
import java.util.Arrays;
import java.util.Collections;
@@ -165,6 +166,8 @@ final class GroupsV2UpdateMessageProducer {
describeGroupMembershipAccessLevelChange(update.groupMembershipAccessLevelChangeUpdate, updates);
} else if (update.groupAttributesAccessLevelChangeUpdate != null) {
describeGroupAttributesAccessLevelChange(update.groupAttributesAccessLevelChangeUpdate, updates);
} else if (update.groupMemberLabelAccessLevelChangeUpdate != null) {
describeGroupMemberLabelAccessLevelChange(update.groupMemberLabelAccessLevelChangeUpdate, updates);
} else if (update.groupAnnouncementOnlyChangeUpdate != null) {
describeGroupAnnouncementOnlyUpdate(update.groupAnnouncementOnlyChangeUpdate, updates);
} else if (update.groupAdminStatusUpdate != null) {
@@ -592,6 +595,24 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeGroupMemberLabelAccessLevelChange(@NonNull GroupMemberLabelAccessLevelChangeUpdate update, @NonNull List<UpdateDescription> updates) {
if (update.accessLevel == GroupV2AccessLevel.UNKNOWN) {
return;
}
String accessLevel = GV2AccessLevelUtil.toString(context, backupGv2AccessLevelToGroups(update.accessLevel));
if (update.updaterAci == null) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_unknown_admin_changed_who_can_add_member_labels_to_s, accessLevel), Glyph.MEGAPHONE));
} else {
boolean editorIsYou = selfIds.matches(update.updaterAci);
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_add_member_labels_to_s, accessLevel), Glyph.MEGAPHONE));
} else {
updates.add(updateDescription(R.string.MessageRecord_s_changed_who_can_add_member_labels_to_s, update.updaterAci, accessLevel, Glyph.MEGAPHONE));
}
}
}
private void describeGroupAnnouncementOnlyUpdate(@NonNull GroupAnnouncementOnlyChangeUpdate update, @NonNull List<UpdateDescription> updates) {
if (update.updaterAci == null) {
if (update.isAnnouncementOnly) {
@@ -707,6 +728,7 @@ final class GroupsV2UpdateMessageProducer {
describeUnknownEditorNewTimer(change, updates);
describeUnknownEditorNewAttributeAccess(change, updates);
describeUnknownEditorNewMembershipAccess(change, updates);
describeUnknownEditorNewMemberLabelAccess(change, updates);
describeUnknownEditorNewGroupInviteLinkAccess(previousGroupState, change, updates);
describeRequestingMembers(change, updates);
describeUnknownEditorRequestingMembersApprovals(change, updates);
@@ -733,6 +755,7 @@ final class GroupsV2UpdateMessageProducer {
describeNewTimer(change, updates);
describeNewAttributeAccess(change, updates);
describeNewMembershipAccess(change, updates);
describeNewMemberLabelAccess(change, updates);
describeNewGroupInviteLinkAccess(previousGroupState, change, updates);
describeRequestingMembers(change, updates);
describeRequestingMembersApprovals(change, updates);
@@ -1223,6 +1246,26 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeNewMemberLabelAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = selfIds.matches(change.editorServiceIdBytes);
if (change.newMemberLabelAccess != AccessControl.AccessRequired.UNKNOWN) {
String accessLevel = GV2AccessLevelUtil.toString(context, change.newMemberLabelAccess);
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_add_member_labels_to_s, accessLevel), Glyph.MEGAPHONE));
} else {
updates.add(updateDescription(R.string.MessageRecord_s_changed_who_can_add_member_labels_to_s, change.editorServiceIdBytes, accessLevel, Glyph.MEGAPHONE));
}
}
}
private void describeUnknownEditorNewMemberLabelAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
if (change.newMemberLabelAccess != AccessControl.AccessRequired.UNKNOWN) {
String accessLevel = GV2AccessLevelUtil.toString(context, change.newMemberLabelAccess);
updates.add(updateDescription(context.getString(R.string.MessageRecord_unknown_admin_changed_who_can_add_member_labels_to_s, accessLevel), Glyph.MEGAPHONE));
}
}
private void describeNewGroupInviteLinkAccess(@Nullable DecryptedGroup previousGroupState,
@NonNull DecryptedGroupChange change,
@NonNull List<UpdateDescription> updates)

View File

@@ -60,6 +60,7 @@ import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.SignalTrace
import org.thoughtcrime.securesms.util.asChain
import org.whispersystems.signalservice.api.InvalidMessageStructureException
import org.whispersystems.signalservice.api.crypto.ContentHint
@@ -73,7 +74,6 @@ import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.Envelope
import org.whispersystems.signalservice.internal.push.PniSignatureMessage
import org.whispersystems.signalservice.internal.util.Util
import org.thoughtcrime.securesms.util.SignalTrace
import java.util.Optional
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.DurationUnit

View File

@@ -2022,6 +2022,13 @@
<string name="MessageRecord_s_changed_who_can_edit_group_membership_to_s">%1$s changed who can edit group membership to \"%2$s\".</string>
<string name="MessageRecord_who_can_edit_group_membership_has_been_changed_to_s">Who can edit group membership has been changed to \"%1$s\".</string>
<!-- Shown when the current user changes the member label permission. -->
<string name="MessageRecord_you_changed_who_can_add_member_labels_to_s">You changed who can add member labels to \"%1$s\".</string>
<!-- Shown when another group member changes the member label permission. -->
<string name="MessageRecord_s_changed_who_can_add_member_labels_to_s">%1$s changed who can add member labels to \"%2$s\".</string>
<!-- Shown when the member label permission is changed by an unknown admin. -->
<string name="MessageRecord_unknown_admin_changed_who_can_add_member_labels_to_s">An admin changed who can add member labels to \"%1$s\".</string>
<!-- GV2 announcement group change -->
<string name="MessageRecord_you_allow_all_members_to_send">You changed the group settings to allow all members to send messages.</string>
<string name="MessageRecord_you_allow_only_admins_to_send">You changed the group settings to only allow admins to send messages.</string>

View File

@@ -824,6 +824,34 @@ class GroupsV2UpdateMessageProducerTest {
assertEquals(listOf("Who can edit group membership has been changed to \"Only admins\"."), describeChange(change))
}
// member label access change
@Test
fun member_changes_member_label_access() {
val change = ChangeBuilder.changeBy(bob)
.memberLabelAccess(MEMBER)
.build()
assertEquals(listOf("Bob changed who can add member labels to \"All members\"."), describeChange(change))
}
@Test
fun you_changed_member_label_access() {
val change = ChangeBuilder.changeBy(you)
.memberLabelAccess(ADMINISTRATOR)
.build()
assertEquals(listOf("You changed who can add member labels to \"Only admins\"."), describeChange(change))
}
@Test
fun unknown_changed_member_label_access() {
val change = ChangeBuilder.changeByUnknown()
.memberLabelAccess(ADMINISTRATOR)
.build()
assertEquals(listOf("An admin changed who can add member labels to \"Only admins\"."), describeChange(change))
}
// Group link access change
@Test
fun you_changed_group_link_access_to_any() {

View File

@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.groups.v2;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.models.ServiceId.ACI;
import org.signal.core.util.Util;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.storageservice.storage.protos.groups.AccessControl;
@@ -16,8 +18,6 @@ import org.signal.storageservice.storage.protos.groups.local.DecryptedPendingMem
import org.signal.storageservice.storage.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.storage.protos.groups.local.DecryptedString;
import org.signal.storageservice.storage.protos.groups.local.DecryptedTimer;
import org.signal.core.util.Util;
import org.signal.core.models.ServiceId.ACI;
import kotlin.collections.CollectionsKt;
import okio.ByteString;
@@ -128,6 +128,11 @@ public final class ChangeBuilder {
return this;
}
public ChangeBuilder memberLabelAccess(@NonNull AccessControl.AccessRequired accessRequired) {
builder.newMemberLabelAccess(accessRequired);
return this;
}
public ChangeBuilder inviteLinkAccess(@NonNull AccessControl.AccessRequired accessRequired) {
builder.newInviteLinkAccess(accessRequired);
return this;