diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index fb8e8ad08f..cb72faae61 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -12,9 +12,11 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; import org.signal.zkgroup.VerificationFailedException; import org.signal.zkgroup.groups.GroupMasterKey; +import org.signal.zkgroup.groups.GroupSecretParams; import org.signal.zkgroup.groups.UuidCiphertext; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl; import org.thoughtcrime.securesms.groups.v2.GroupLinkPassword; import org.thoughtcrime.securesms.keyvalue.SignalStore; @@ -22,9 +24,11 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; +import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import java.io.IOException; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -157,11 +161,11 @@ public final class GroupManager { } @WorkerThread - public static void ejectFromGroup(@NonNull Context context, @NonNull GroupId.V2 groupId, @NonNull Recipient recipient) + public static void ejectAndBanFromGroup(@NonNull Context context, @NonNull GroupId.V2 groupId, @NonNull Recipient recipient) throws GroupChangeBusyException, GroupChangeFailedException, GroupInsufficientRightsException, GroupNotAMemberException, IOException { try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) { - edit.ejectMember(recipient.requireServiceId(), false); + edit.ejectMember(recipient.requireServiceId(), false, true); Log.i(TAG, "Member removed from group " + groupId); } } @@ -273,6 +277,28 @@ public final class GroupManager { } } + @WorkerThread + public static void ban(@NonNull Context context, + @NonNull GroupId.V2 groupId, + @NonNull RecipientId recipientId) + throws GroupChangeBusyException, IOException, GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException + { + try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { + editor.ban(Collections.singleton(Recipient.resolved(recipientId).requireServiceId().uuid())); + } + } + + @WorkerThread + public static void unban(@NonNull Context context, + @NonNull GroupId.V2 groupId, + @NonNull RecipientId recipientId) + throws GroupChangeBusyException, IOException, GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException + { + try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { + editor.unban(Collections.singleton(Recipient.resolved(recipientId).requireServiceId().uuid())); + } + } + @WorkerThread public static void applyMembershipAdditionRightsChange(@NonNull Context context, @NonNull GroupId.V2 groupId, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index 66c457b8c6..737b6e0f0a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -428,7 +428,7 @@ final class GroupManagerV2 { .map(r -> Recipient.resolved(r).requireServiceId().uuid()) .collect(Collectors.toSet()); - return commitChangeWithConflictResolution(groupOperations.createRefuseGroupJoinRequest(uuids)); + return commitChangeWithConflictResolution(groupOperations.createRefuseGroupJoinRequest(uuids, true)); } @WorkerThread @@ -455,15 +455,15 @@ final class GroupManagerV2 { throw new AssertionError(e); } } else { - return ejectMember(ServiceId.from(selfAci.uuid()), true); + return ejectMember(ServiceId.from(selfAci.uuid()), true, false); } } @WorkerThread - @NonNull GroupManager.GroupActionResult ejectMember(@NonNull ServiceId serviceId, boolean allowWhenBlocked) + @NonNull GroupManager.GroupActionResult ejectMember(@NonNull ServiceId serviceId, boolean allowWhenBlocked, boolean ban) throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException { - return commitChangeWithConflictResolution(groupOperations.createRemoveMembersChange(Collections.singleton(serviceId.uuid())), allowWhenBlocked); + return commitChangeWithConflictResolution(groupOperations.createRemoveMembersChange(Collections.singleton(serviceId.uuid()), ban), allowWhenBlocked); } @WorkerThread @@ -530,6 +530,14 @@ final class GroupManagerV2 { return commitChangeWithConflictResolution(groupOperations.createAcceptInviteChange(groupCandidate.getProfileKeyCredential().get())); } + public GroupManager.GroupActionResult ban(Set uuids) throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException { + return commitChangeWithConflictResolution(groupOperations.createBanUuidsChange(uuids)); + } + + public GroupManager.GroupActionResult unban(Set uuids) throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException { + return commitChangeWithConflictResolution(groupOperations.createUnbanUuidsChange(uuids)); + } + @WorkerThread public GroupManager.GroupActionResult cycleGroupLinkPassword() throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException @@ -1094,7 +1102,7 @@ final class GroupManagerV2 { GroupChange signedGroupChange; try { - signedGroupChange = commitCancelChangeWithConflictResolution(groupOperations.createRefuseGroupJoinRequest(uuids)); + signedGroupChange = commitCancelChangeWithConflictResolution(groupOperations.createRefuseGroupJoinRequest(uuids, false)); } catch (GroupLinkNotActiveException e) { Log.d(TAG, "Unexpected unable to leave group due to group link off"); throw new GroupChangeFailedException(e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestConfirmationDialog.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestConfirmationDialog.java index d6329faf74..a9856c71d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestConfirmationDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestConfirmationDialog.java @@ -5,6 +5,8 @@ import android.content.Context; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.recipients.Recipient; @@ -13,50 +15,37 @@ final class RequestConfirmationDialog { private RequestConfirmationDialog() { } - /** - * Confirms that you want to approve or deny a request to join the group depending on - * {@param approve}. - */ - static AlertDialog show(@NonNull Context context, - @NonNull Recipient requester, - boolean approve, - @NonNull Runnable onApproveOrDeny) - { - if (approve) { - return showRequestApproveConfirmationDialog(context, requester, onApproveOrDeny); - } else { - return showRequestDenyConfirmationDialog(context, requester, onApproveOrDeny); - } - } - /** * Confirms that you want to approve a request to join the group. */ - private static AlertDialog showRequestApproveConfirmationDialog(@NonNull Context context, - @NonNull Recipient requester, - @NonNull Runnable onApprove) + public static AlertDialog showApprove(@NonNull Context context, + @NonNull Recipient requester, + @NonNull Runnable onApprove) { - return new AlertDialog.Builder(context) - .setMessage(context.getString(R.string.RequestConfirmationDialog_add_s_to_the_group, - requester.getDisplayName(context))) - .setPositiveButton(R.string.RequestConfirmationDialog_add, (dialog, which) -> onApprove.run()) - .setNegativeButton(android.R.string.cancel, null) - .show(); + return new MaterialAlertDialogBuilder(context) + .setMessage(context.getString(R.string.RequestConfirmationDialog_add_s_to_the_group, + requester.getDisplayName(context))) + .setPositiveButton(R.string.RequestConfirmationDialog_add, (dialog, which) -> onApprove.run()) + .setNegativeButton(android.R.string.cancel, null) + .show(); } /** * Confirms that you want to deny a request to join the group. */ - private static AlertDialog showRequestDenyConfirmationDialog(@NonNull Context context, - @NonNull Recipient requester, - @NonNull Runnable onDeny) + public static AlertDialog showDeny(@NonNull Context context, + @NonNull Recipient requester, + boolean linkEnabled, + @NonNull Runnable onDeny) { - return new AlertDialog.Builder(context) - .setMessage(context.getString(R.string.RequestConfirmationDialog_deny_request_from_s, - requester.getDisplayName(context))) - .setPositiveButton(R.string.RequestConfirmationDialog_deny, (dialog, which) -> onDeny.run()) - .setNegativeButton(android.R.string.cancel, null) - .show(); + String message = linkEnabled ? context.getString(R.string.RequestConfirmationDialog_deny_request_from_s_they_will_not_be_able_to_request, requester.getDisplayName(context)) + : context.getString(R.string.RequestConfirmationDialog_deny_request_from_s, requester.getDisplayName(context)); + + return new MaterialAlertDialogBuilder(context) + .setMessage(message) + .setPositiveButton(R.string.RequestConfirmationDialog_deny, (dialog, which) -> onDeny.run()) + .setNegativeButton(android.R.string.cancel, null) + .show(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestingMemberInvitesViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestingMemberInvitesViewModel.java index 0bcd62835c..acc4e98e59 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestingMemberInvitesViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestingMemberInvitesViewModel.java @@ -15,13 +15,11 @@ import org.thoughtcrime.securesms.groups.LiveGroup; import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; import org.thoughtcrime.securesms.groups.ui.GroupErrors; import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry; -import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.groups.v2.GroupLinkUrlAndStatus; import org.thoughtcrime.securesms.util.AsynchronousCallback; import org.thoughtcrime.securesms.util.SingleLiveEvent; -import java.util.Collections; import java.util.List; -import java.util.Set; public class RequestingMemberInvitesViewModel extends ViewModel { @@ -29,6 +27,7 @@ public class RequestingMemberInvitesViewModel extends ViewModel { private final RequestingMemberRepository requestingMemberRepository; private final MutableLiveData toasts; private final LiveData> requesting; + private final LiveData inviteLink; private RequestingMemberInvitesViewModel(@NonNull Context context, @NonNull GroupId.V2 groupId, @@ -36,62 +35,58 @@ public class RequestingMemberInvitesViewModel extends ViewModel { { this.context = context; this.requestingMemberRepository = requestingMemberRepository; - this.requesting = new LiveGroup(groupId).getRequestingMembers(); this.toasts = new SingleLiveEvent<>(); + + LiveGroup liveGroup = new LiveGroup(groupId); + + this.requesting = liveGroup.getRequestingMembers(); + this.inviteLink = liveGroup.getGroupLink(); } LiveData> getRequesting() { return requesting; } + LiveData getInviteLink() { + return inviteLink; + } + LiveData getToasts() { return toasts; } void approveRequestFor(@NonNull GroupMemberEntry.RequestingMember requestingMember) { - approveOrDeny(requestingMember, true); + requestingMember.setBusy(true); + requestingMemberRepository.approveRequest(requestingMember.getRequester(), new AsynchronousCallback.WorkerThread() { + @Override + public void onComplete(@Nullable Void result) { + requestingMember.setBusy(false); + toasts.postValue(context.getString(R.string.RequestingMembersFragment_added_s, requestingMember.getRequester().getDisplayName(context))); + } + + @Override + public void onError(@Nullable GroupChangeFailureReason error) { + requestingMember.setBusy(false); + toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error))); + } + }); } void denyRequestFor(@NonNull GroupMemberEntry.RequestingMember requestingMember) { - approveOrDeny(requestingMember, false); - } + requestingMember.setBusy(true); + requestingMemberRepository.denyRequest(requestingMember.getRequester(), new AsynchronousCallback.WorkerThread() { + @Override + public void onComplete(@Nullable Void result) { + requestingMember.setBusy(false); + toasts.postValue(context.getString(R.string.RequestingMembersFragment_denied_s, requestingMember.getRequester().getDisplayName(context))); + } - private void approveOrDeny(@NonNull GroupMemberEntry.RequestingMember requestingMember, boolean approve) { - RequestConfirmationDialog.show(context, requestingMember.getRequester(), approve, () -> { - Set memberAsSet = Collections.singleton(requestingMember.getRequester().getId()); - - if (approve) { - requestingMember.setBusy(true); - requestingMemberRepository.approveRequests(memberAsSet, new AsynchronousCallback.WorkerThread() { - @Override - public void onComplete(@Nullable Void result) { - requestingMember.setBusy(false); - toasts.postValue(context.getString(R.string.RequestingMembersFragment_added_s, requestingMember.getRequester().getDisplayName(context))); - } - - @Override - public void onError(@Nullable GroupChangeFailureReason error) { - requestingMember.setBusy(false); - toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error))); - } - }); - } else { - requestingMember.setBusy(true); - requestingMemberRepository.denyRequests(memberAsSet, new AsynchronousCallback.WorkerThread() { - @Override - public void onComplete(@Nullable Void result) { - requestingMember.setBusy(false); - toasts.postValue(context.getString(R.string.RequestingMembersFragment_denied_s, requestingMember.getRequester().getDisplayName(context))); - } - - @Override - public void onError(@Nullable GroupChangeFailureReason error) { - requestingMember.setBusy(false); - toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error))); - } - }); - } - }); + @Override + public void onError(@Nullable GroupChangeFailureReason error) { + requestingMember.setBusy(false); + toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error))); + } + }); } public static class Factory implements ViewModelProvider.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestingMemberRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestingMemberRepository.java index ae2d811e2f..038714f802 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestingMemberRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestingMemberRepository.java @@ -10,11 +10,13 @@ import org.thoughtcrime.securesms.groups.GroupChangeException; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupManager; import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; +import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.AsynchronousCallback; import java.io.IOException; import java.util.Collection; +import java.util.Collections; /** * Repository for modifying the requesting members on a single group. @@ -31,12 +33,12 @@ final class RequestingMemberRepository { this.groupId = groupId; } - void approveRequests(@NonNull Collection recipientIds, - @NonNull AsynchronousCallback.WorkerThread callback) + void approveRequest(@NonNull Recipient recipient, + @NonNull AsynchronousCallback.WorkerThread callback) { SignalExecutors.UNBOUNDED.execute(() -> { try { - GroupManager.approveRequests(context, groupId, recipientIds); + GroupManager.approveRequests(context, groupId, Collections.singleton(recipient.getId())); callback.onComplete(null); } catch (GroupChangeException | IOException e) { Log.w(TAG, e); @@ -45,12 +47,12 @@ final class RequestingMemberRepository { }); } - void denyRequests(@NonNull Collection recipientIds, - @NonNull AsynchronousCallback.WorkerThread callback) + void denyRequest(@NonNull Recipient recipient, + @NonNull AsynchronousCallback.WorkerThread callback) { SignalExecutors.UNBOUNDED.execute(() -> { try { - GroupManager.denyRequests(context, groupId, recipientIds); + GroupManager.denyRequests(context, groupId, Collections.singleton(recipient.getId())); callback.onComplete(null); } catch (GroupChangeException | IOException e) { Log.w(TAG, e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestingMembersFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestingMembersFragment.java index b99fb1ecf6..4707f464b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestingMembersFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/requesting/RequestingMembersFragment.java @@ -9,6 +9,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProviders; import org.thoughtcrime.securesms.R; @@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.ui.AdminActionsListener; import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry; import org.thoughtcrime.securesms.groups.ui.GroupMemberListView; +import org.thoughtcrime.securesms.groups.v2.GroupLinkUrlAndStatus; import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment; import org.thoughtcrime.securesms.util.BottomSheetUtil; @@ -29,9 +31,9 @@ public class RequestingMembersFragment extends Fragment { private static final String GROUP_ID = "GROUP_ID"; private RequestingMemberInvitesViewModel viewModel; - private GroupMemberListView requestingMembers; - private View noRequestingMessage; - private View requestingExplanation; + private GroupMemberListView requestingMembers; + private View noRequestingMessage; + private View requestingExplanation; public static RequestingMembersFragment newInstance(@NonNull GroupId.V2 groupId) { RequestingMembersFragment fragment = new RequestingMembersFragment(); @@ -53,9 +55,10 @@ public class RequestingMembersFragment extends Fragment { requestingMembers.initializeAdapter(getViewLifecycleOwner()); - requestingMembers.setRecipientClickListener(recipient -> + requestingMembers.setRecipientClickListener(recipient -> { RecipientBottomSheetDialogFragment.create(recipient.getId(), null) - .show(requireActivity().getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)); + .show(requireActivity().getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); + }); requestingMembers.setAdminActionsListener(new AdminActionsListener() { @@ -71,31 +74,37 @@ public class RequestingMembersFragment extends Fragment { @Override public void onApproveRequest(@NonNull GroupMemberEntry.RequestingMember requestingMember) { - viewModel.approveRequestFor(requestingMember); + //noinspection CodeBlock2Expr + RequestConfirmationDialog.showApprove(requireContext(), requestingMember.getRequester(), () -> { + viewModel.approveRequestFor(requestingMember); + }); } @Override public void onDenyRequest(@NonNull GroupMemberEntry.RequestingMember requestingMember) { - viewModel.denyRequestFor(requestingMember); + GroupLinkUrlAndStatus linkStatus = viewModel.getInviteLink().getValue(); + boolean linkEnabled = linkStatus == null || linkStatus.isEnabled(); + + //noinspection CodeBlock2Expr + RequestConfirmationDialog.showDeny(requireContext(), requestingMember.getRequester(), linkEnabled, () -> { + viewModel.denyRequestFor(requestingMember); + }); } }); return view; } - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - + @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { GroupId.V2 groupId = GroupId.parseOrThrow(Objects.requireNonNull(requireArguments().getString(GROUP_ID))).requireV2(); RequestingMemberInvitesViewModel.Factory factory = new RequestingMemberInvitesViewModel.Factory(requireContext(), groupId); - viewModel = ViewModelProviders.of(requireActivity(), factory).get(RequestingMemberInvitesViewModel.class); + viewModel = new ViewModelProvider(this, factory).get(RequestingMemberInvitesViewModel.class); viewModel.getRequesting().observe(getViewLifecycleOwner(), requesting -> { requestingMembers.setMembers(requesting); - noRequestingMessage.setVisibility(requesting.isEmpty() ? View.VISIBLE: View.GONE); + noRequestingMessage.setVisibility(requesting.isEmpty() ? View.VISIBLE : View.GONE); requestingExplanation.setVisibility(requesting.isEmpty() ? View.GONE : View.VISIBLE); }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/spoofing/ReviewCardRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/spoofing/ReviewCardRepository.java index f7df689294..d55fe563d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/spoofing/ReviewCardRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/spoofing/ReviewCardRepository.java @@ -101,7 +101,7 @@ class ReviewCardRepository { SignalExecutors.BOUNDED.execute(() -> { try { - GroupManager.ejectFromGroup(context, groupId, reviewCard.getReviewRecipient()); + GroupManager.ejectAndBanFromGroup(context, groupId, reviewCard.getReviewRecipient()); onRemoveFromGroupListener.onActionCompleted(); } catch (GroupChangeException | IOException e) { onRemoveFromGroupListener.onActionFailed(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java index e80d67873b..50612d11e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java @@ -290,6 +290,10 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF makeGroupAdminButton.setVisibility(adminStatus.isCanMakeAdmin() ? View.VISIBLE : View.GONE); removeAdminButton.setVisibility(adminStatus.isCanMakeNonAdmin() ? View.VISIBLE : View.GONE); removeFromGroupButton.setVisibility(adminStatus.isCanRemove() ? View.VISIBLE : View.GONE); + + if (adminStatus.isCanRemove()) { + removeFromGroupButton.setOnClickListener(view -> viewModel.onRemoveFromGroupClicked(requireActivity(), adminStatus.isLinkActive(), this::dismiss)); + } }); viewModel.getIdentity().observe(getViewLifecycleOwner(), identityRecord -> { @@ -319,8 +323,6 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF makeGroupAdminButton.setOnClickListener(view -> viewModel.onMakeGroupAdminClicked(requireActivity())); removeAdminButton.setOnClickListener(view -> viewModel.onRemoveGroupAdminClicked(requireActivity())); - removeFromGroupButton.setOnClickListener(view -> viewModel.onRemoveFromGroupClicked(requireActivity(), this::dismiss)); - addToGroupButton.setOnClickListener(view -> { dismiss(); viewModel.onAddToGroupButton(requireActivity()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java index 42e4798669..76dfff2ec9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java @@ -77,7 +77,7 @@ final class RecipientDialogRepository { SimpleTask.run(SignalExecutors.UNBOUNDED, () -> { try { - GroupManager.ejectFromGroup(context, Objects.requireNonNull(groupId).requireV2(), Recipient.resolved(recipientId)); + GroupManager.ejectAndBanFromGroup(context, Objects.requireNonNull(groupId).requireV2(), Recipient.resolved(recipientId)); return true; } catch (GroupChangeException | IOException e) { Log.w(TAG, e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java index 68739a59d1..cabf3d29e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java @@ -15,6 +15,8 @@ import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + import org.signal.core.util.ThreadUtil; import org.thoughtcrime.securesms.BlockUnblockDialog; import org.thoughtcrime.securesms.R; @@ -33,6 +35,7 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.thoughtcrime.securesms.verify.VerifyIdentityActivity; +import org.whispersystems.libsignal.util.Pair; import java.util.Objects; @@ -68,20 +71,22 @@ final class RecipientDialogViewModel extends ViewModel { if (recipientDialogRepository.getGroupId() != null && recipientDialogRepository.getGroupId().isV2() && !recipientIsSelf) { LiveGroup source = new LiveGroup(recipientDialogRepository.getGroupId()); - LiveData localIsAdmin = source.isSelfAdmin(); + LiveData> localStatus = LiveDataUtil.combineLatest(source.isSelfAdmin(), Transformations.map(source.getGroupLink(), s -> s == null || s.isEnabled()), Pair::new); LiveData recipientMemberLevel = Transformations.switchMap(recipient, source::getMemberLevel); - adminActionStatus = LiveDataUtil.combineLatest(localIsAdmin, recipientMemberLevel, - (localAdmin, memberLevel) -> { - boolean inGroup = memberLevel.isInGroup(); - boolean recipientAdmin = memberLevel == GroupDatabase.MemberLevel.ADMINISTRATOR; + adminActionStatus = LiveDataUtil.combineLatest(localStatus, recipientMemberLevel, (statuses, memberLevel) -> { + boolean localAdmin = statuses.first(); + boolean isLinkActive = statuses.second(); + boolean inGroup = memberLevel.isInGroup(); + boolean recipientAdmin = memberLevel == GroupDatabase.MemberLevel.ADMINISTRATOR; - return new AdminActionStatus(inGroup && localAdmin, - inGroup && localAdmin && !recipientAdmin, - inGroup && localAdmin && recipientAdmin); - }); + return new AdminActionStatus(inGroup && localAdmin, + inGroup && localAdmin && !recipientAdmin, + inGroup && localAdmin && recipientAdmin, + isLinkActive); + }); } else { - adminActionStatus = new MutableLiveData<>(new AdminActionStatus(false, false, false)); + adminActionStatus = new MutableLiveData<>(new AdminActionStatus(false, false, false, false)); } boolean isSelf = recipientDialogRepository.getRecipientId().equals(Recipient.self().getId()); @@ -164,7 +169,7 @@ final class RecipientDialogViewModel extends ViewModel { } void onMakeGroupAdminClicked(@NonNull Activity activity) { - new AlertDialog.Builder(activity) + new MaterialAlertDialogBuilder(activity) .setMessage(context.getString(R.string.RecipientBottomSheet_s_will_be_able_to_edit_group, Objects.requireNonNull(recipient.getValue()).getDisplayName(context))) .setPositiveButton(R.string.RecipientBottomSheet_make_admin, (dialog, which) -> { @@ -182,7 +187,7 @@ final class RecipientDialogViewModel extends ViewModel { } void onRemoveGroupAdminClicked(@NonNull Activity activity) { - new AlertDialog.Builder(activity) + new MaterialAlertDialogBuilder(activity) .setMessage(context.getString(R.string.RecipientBottomSheet_remove_s_as_group_admin, Objects.requireNonNull(recipient.getValue()).getDisplayName(context))) .setPositiveButton(R.string.RecipientBottomSheet_remove_as_admin, (dialog, which) -> { @@ -199,9 +204,11 @@ final class RecipientDialogViewModel extends ViewModel { .show(); } - void onRemoveFromGroupClicked(@NonNull Activity activity, @NonNull Runnable onSuccess) { - new AlertDialog.Builder(activity) - .setMessage(context.getString(R.string.RecipientBottomSheet_remove_s_from_the_group, Objects.requireNonNull(recipient.getValue()).getDisplayName(context))) + void onRemoveFromGroupClicked(@NonNull Activity activity, boolean isLinkActive, @NonNull Runnable onSuccess) { + new MaterialAlertDialogBuilder(activity) + .setMessage(context.getString(isLinkActive ? R.string.RecipientBottomSheet_remove_s_from_the_group_they_will_not_be_able_to_rejoin + : R.string.RecipientBottomSheet_remove_s_from_the_group, + Objects.requireNonNull(recipient.getValue()).getDisplayName(context))) .setPositiveButton(R.string.RecipientBottomSheet_remove, (dialog, which) -> { adminActionBusy.setValue(true); @@ -234,11 +241,13 @@ final class RecipientDialogViewModel extends ViewModel { private final boolean canRemove; private final boolean canMakeAdmin; private final boolean canMakeNonAdmin; + private final boolean isLinkActive; - AdminActionStatus(boolean canRemove, boolean canMakeAdmin, boolean canMakeNonAdmin) { + AdminActionStatus(boolean canRemove, boolean canMakeAdmin, boolean canMakeNonAdmin, boolean isLinkActive) { this.canRemove = canRemove; this.canMakeAdmin = canMakeAdmin; this.canMakeNonAdmin = canMakeNonAdmin; + this.isLinkActive = isLinkActive; } boolean isCanRemove() { @@ -252,6 +261,10 @@ final class RecipientDialogViewModel extends ViewModel { boolean isCanMakeNonAdmin() { return canMakeNonAdmin; } + + boolean isLinkActive() { + return isLinkActive; + } } public static class Factory implements ViewModelProvider.Factory { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d1ae95a87b..1c48331a26 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -913,6 +913,8 @@ Add “%1$s” to the group? Deny request from “%1$s”? + + Deny request from “%1$s”? They will not be able to request to join via the group link again. Add Deny @@ -3350,6 +3352,8 @@ "%1$s" will be able to edit this group and its members. Remove %1$s from the group? + + Remove %1$s from the group? They will not be able to rejoin via the group link. Remove Copied to clipboard diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ChangeSetModifier.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ChangeSetModifier.java index 51b348b2af..1f01ea975a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ChangeSetModifier.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ChangeSetModifier.java @@ -40,4 +40,8 @@ public interface ChangeSetModifier { void clearModifyDescription(); void clearModifyAnnouncementsOnly(); + + void removeAddBannedMembers(int i); + + void removeDeleteBannedMembers(int i); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupChangeActionsBuilderChangeSetModifier.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupChangeActionsBuilderChangeSetModifier.java index 2b703ce3f0..0151c7b389 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupChangeActionsBuilderChangeSetModifier.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupChangeActionsBuilderChangeSetModifier.java @@ -133,6 +133,16 @@ final class DecryptedGroupChangeActionsBuilderChangeSetModifier implements Chang result.clearNewIsAnnouncementGroup(); } + @Override + public void removeAddBannedMembers(int i) { + result.removeNewBannedMembers(i); + } + + @Override + public void removeDeleteBannedMembers(int i) { + result.removeDeleteBannedMembers(i); + } + private static List removeIndexFromByteStringList(List byteStrings, int i) { List modifiedList = new ArrayList<>(byteStrings); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java index b7ed15e5b5..7eae0ccf66 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java @@ -5,6 +5,7 @@ import com.google.protobuf.ByteString; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedApproveMember; +import org.signal.storageservice.protos.groups.local.DecryptedBannedMember; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedMember; @@ -297,6 +298,10 @@ public final class DecryptedGroupUtil { applyInviteLinkPassword(builder, change); + applyAddBannedMembersActions(builder, change.getNewBannedMembersList()); + + applyDeleteBannedMembersActions(builder, change.getDeleteBannedMembersList()); + return builder.build(); } @@ -505,6 +510,31 @@ public final class DecryptedGroupUtil { } } + private static void applyAddBannedMembersActions(DecryptedGroup.Builder builder, List newBannedMembersList) { + Set bannedMemberUuidSet = getBannedMemberUuidSet(builder.getBannedMembersList()); + + for (DecryptedBannedMember member : newBannedMembersList) { + if (bannedMemberUuidSet.contains(member.getUuid())) { + Log.w(TAG, "Banned member already in banned list"); + } else { + builder.addBannedMembers(member); + } + } + } + + private static void applyDeleteBannedMembersActions(DecryptedGroup.Builder builder, List deleteMembersList) { + for (DecryptedBannedMember removedMember : deleteMembersList) { + int index = indexOfUuidInBannedMemberList(builder.getBannedMembersList(), removedMember.getUuid()); + + if (index == -1) { + Log.w(TAG, "Deleted banned member on change not found in banned list"); + continue; + } + + builder.removeBannedMembers(index); + } + } + private static DecryptedMember withNewProfileKey(DecryptedMember member, ByteString profileKey) { return DecryptedMember.newBuilder(member) .setProfileKey(profileKey) @@ -531,6 +561,16 @@ public final class DecryptedGroupUtil { return pendingMemberCipherTexts; } + private static Set getBannedMemberUuidSet(List bannedMemberList) { + Set memberUuids = new HashSet<>(bannedMemberList.size()); + + for (DecryptedBannedMember member : bannedMemberList) { + memberUuids.add(member.getUuid()); + } + + return memberUuids; + } + private static void removePendingAndRequestingMembersNowInGroup(DecryptedGroup.Builder builder) { Set allMembers = membersToUuidByteStringSet(builder.getMembersList()); @@ -569,6 +609,13 @@ public final class DecryptedGroupUtil { return -1; } + private static int indexOfUuidInBannedMemberList(List memberList, ByteString uuid) { + for (int i = 0; i < memberList.size(); i++) { + if (uuid.equals(memberList.get(i).getUuid())) return i; + } + return -1; + } + public static Optional findInviter(List pendingMembersList, UUID uuid) { return Optional.fromNullable(findPendingByUuid(pendingMembersList, uuid).transform(DecryptedPendingMember::getAddedByUuid) .transform(UuidUtil::fromByteStringOrNull) @@ -598,7 +645,9 @@ public final class DecryptedGroupUtil { change.getPromoteRequestingMembersCount() == 0 && // field 18 change.getNewInviteLinkPassword().size() == 0 && // field 19 !change.hasNewDescription() && // field 20 - isEmpty(change.getNewIsAnnouncementGroup()); // field 20 + isEmpty(change.getNewIsAnnouncementGroup()) && // field 21 + change.getNewBannedMembersCount() == 0 && // field 22 + change.getDeleteBannedMembersCount() == 0; // field 23 } static boolean isEmpty(AccessControl.AccessRequired newAttributeAccess) { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeActionsBuilderChangeSetModifier.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeActionsBuilderChangeSetModifier.java index 10ae4649cc..f94ff3fea3 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeActionsBuilderChangeSetModifier.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeActionsBuilderChangeSetModifier.java @@ -112,4 +112,14 @@ final class GroupChangeActionsBuilderChangeSetModifier implements ChangeSetModif public void clearModifyAnnouncementsOnly() { result.clearModifyAnnouncementsOnly(); } + + @Override + public void removeAddBannedMembers(int i) { + result.removeAddBannedMembers(i); + } + + @Override + public void removeDeleteBannedMembers(int i) { + result.removeDeleteBannedMembers(i); + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeReconstruct.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeReconstruct.java index 572963cf7c..d968baf689 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeReconstruct.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeReconstruct.java @@ -3,6 +3,7 @@ package org.whispersystems.signalservice.api.groupsv2; import com.google.protobuf.ByteString; import org.signal.storageservice.protos.groups.local.DecryptedApproveMember; +import org.signal.storageservice.protos.groups.local.DecryptedBannedMember; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedMember; @@ -65,12 +66,17 @@ public final class GroupChangeReconstruct { Set requestingMembersListA = requestingMembersToSetOfUuids(fromState.getRequestingMembersList()); Set requestingMembersListB = requestingMembersToSetOfUuids(toState.getRequestingMembersList()); + Set bannedMembersListA = bannedMembersToSetOfUuids(fromState.getBannedMembersList()); + Set bannedMembersListB = bannedMembersToSetOfUuids(toState.getBannedMembersList()); + Set removedPendingMemberUuids = subtract(pendingMembersListA, pendingMembersListB); Set removedRequestingMemberUuids = subtract(requestingMembersListA, requestingMembersListB); Set newPendingMemberUuids = subtract(pendingMembersListB, pendingMembersListA); Set newRequestingMemberUuids = subtract(requestingMembersListB, requestingMembersListA); Set removedMemberUuids = subtract(fromStateMemberUuids, toStateMemberUuids); Set newMemberUuids = subtract(toStateMemberUuids, fromStateMemberUuids); + Set removedBannedMemberUuids = subtract(bannedMembersListA, bannedMembersListB); + Set newBannedMemberUuids = subtract(bannedMembersListB, bannedMembersListA); Set addedByInvitationUuids = intersect(newMemberUuids, removedPendingMemberUuids); Set addedByRequestApprovalUuids = intersect(newMemberUuids, removedRequestingMemberUuids); @@ -141,6 +147,14 @@ public final class GroupChangeReconstruct { builder.setNewInviteLinkPassword(toState.getInviteLinkPassword()); } + for (ByteString uuid : removedBannedMemberUuids) { + builder.addDeleteBannedMembers(DecryptedBannedMember.newBuilder().setUuid(uuid).build()); + } + + for (ByteString uuid : newBannedMemberUuids) { + builder.addNewBannedMembers(DecryptedBannedMember.newBuilder().setUuid(uuid).build()); + } + return builder.build(); } @@ -203,6 +217,14 @@ public final class GroupChangeReconstruct { return uuids; } + private static Set bannedMembersToSetOfUuids(Collection bannedMembers) { + Set uuids = new LinkedHashSet<>(bannedMembers.size()); + for (DecryptedBannedMember bannedMember : bannedMembers) { + uuids.add(bannedMember.getUuid()); + } + return uuids; + } + private static Set subtract(Collection a, Collection b) { Set result = new LinkedHashSet<>(a); result.removeAll(b); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil.java index cf155f461d..3075f7efb1 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil.java @@ -4,6 +4,7 @@ import com.google.protobuf.ByteString; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.local.DecryptedApproveMember; +import org.signal.storageservice.protos.groups.local.DecryptedBannedMember; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedMember; @@ -43,7 +44,9 @@ public final class GroupChangeUtil { change.getPromoteRequestingMembersCount() == 0 && // field 18 !change.hasModifyInviteLinkPassword() && // field 19 !change.hasModifyDescription() && // field 20 - !change.hasModifyAnnouncementsOnly(); // field 21 + !change.hasModifyAnnouncementsOnly() && // field 21 + change.getAddBannedMembersCount() == 0 && // field 22 + change.getDeleteBannedMembersCount() == 0; // field 23 } /** @@ -107,6 +110,7 @@ public final class GroupChangeUtil { HashMap fullMembersByUuid = new HashMap<>(groupState.getMembersCount()); HashMap pendingMembersByUuid = new HashMap<>(groupState.getPendingMembersCount()); HashMap requestingMembersByUuid = new HashMap<>(groupState.getMembersCount()); + HashMap bannedMembersByUuid = new HashMap<>(groupState.getBannedMembersCount()); for (DecryptedMember member : groupState.getMembersList()) { fullMembersByUuid.put(member.getUuid(), member); @@ -120,6 +124,10 @@ public final class GroupChangeUtil { requestingMembersByUuid.put(member.getUuid(), member); } + for (DecryptedBannedMember member : groupState.getBannedMembersList()) { + bannedMembersByUuid.put(member.getUuid(), member); + } + resolveField3AddMembers (conflictingChange, changeSetModifier, fullMembersByUuid, pendingMembersByUuid); resolveField4DeleteMembers (conflictingChange, changeSetModifier, fullMembersByUuid); resolveField5ModifyMemberRoles (conflictingChange, changeSetModifier, fullMembersByUuid); @@ -138,6 +146,8 @@ public final class GroupChangeUtil { resolveField18PromoteRequestingMembers (conflictingChange, changeSetModifier, requestingMembersByUuid); resolveField20ModifyDescription (groupState, conflictingChange, changeSetModifier); resolveField21ModifyAnnouncementsOnly (groupState, conflictingChange, changeSetModifier); + resolveField22AddBannedMembers (conflictingChange, changeSetModifier, bannedMembersByUuid); + resolveField23DeleteBannedMembers (conflictingChange, changeSetModifier, bannedMembersByUuid); } private static void resolveField3AddMembers(DecryptedGroupChange conflictingChange, ChangeSetModifier result, HashMap fullMembersByUuid, HashMap pendingMembersByUuid) { @@ -319,4 +329,28 @@ public final class GroupChangeUtil { result.clearModifyAnnouncementsOnly(); } } + + private static void resolveField22AddBannedMembers(DecryptedGroupChange conflictingChange, ChangeSetModifier result, HashMap bannedMembersByUuid) { + List newBannedMembersList = conflictingChange.getNewBannedMembersList(); + + for (int i = newBannedMembersList.size() - 1; i >= 0; i--) { + DecryptedBannedMember member = newBannedMembersList.get(i); + + if (bannedMembersByUuid.containsKey(member.getUuid())) { + result.removeAddBannedMembers(i); + } + } + } + + private static void resolveField23DeleteBannedMembers(DecryptedGroupChange conflictingChange, ChangeSetModifier result, HashMap bannedMembersByUuid) { + List deleteBannedMembersList = conflictingChange.getDeleteBannedMembersList(); + + for (int i = deleteBannedMembersList.size() - 1; i >= 0; i--) { + DecryptedBannedMember member = deleteBannedMembersList.get(i); + + if (!bannedMembersByUuid.containsKey(member.getUuid())) { + result.removeDeleteBannedMembers(i); + } + } + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java index 34e9164985..a2b9eb6edd 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java @@ -4,6 +4,7 @@ import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import org.signal.storageservice.protos.groups.AccessControl; +import org.signal.storageservice.protos.groups.BannedMember; import org.signal.storageservice.protos.groups.Group; import org.signal.storageservice.protos.groups.GroupAttributeBlob; import org.signal.storageservice.protos.groups.GroupChange; @@ -12,6 +13,7 @@ import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.PendingMember; import org.signal.storageservice.protos.groups.RequestingMember; import org.signal.storageservice.protos.groups.local.DecryptedApproveMember; +import org.signal.storageservice.protos.groups.local.DecryptedBannedMember; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; @@ -47,6 +49,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; /** * Contains operations to create, modify and validate groups and group changes. @@ -59,7 +62,7 @@ public final class GroupsV2Operations { public static final UUID UNKNOWN_UUID = UuidUtil.UNKNOWN_UUID; /** Highest change epoch this class knows now to decrypt */ - public static final int HIGHEST_KNOWN_EPOCH = 3; + public static final int HIGHEST_KNOWN_EPOCH = 4; private final ServerPublicParams serverPublicParams; private final ClientZkProfileOperations clientZkProfileOperations; @@ -160,7 +163,7 @@ public final class GroupsV2Operations { public GroupChange.Actions.Builder createModifyGroupMembershipChange(Set membersToAdd, UUID selfUuid) { final GroupOperations groupOperations = forGroup(groupSecretParams); - GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder(); + GroupChange.Actions.Builder actions = createUnbanUuidsChange(membersToAdd.stream().map(GroupCandidate::getUuid).collect(Collectors.toSet())); for (GroupCandidate credential : membersToAdd) { Member.Role newMemberRole = Member.Role.DEFAULT; @@ -203,8 +206,9 @@ public final class GroupsV2Operations { return actions; } - public GroupChange.Actions.Builder createRefuseGroupJoinRequest(Set requestsToRemove) { - GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder(); + public GroupChange.Actions.Builder createRefuseGroupJoinRequest(Set requestsToRemove, boolean alsoBan) { + GroupChange.Actions.Builder actions = alsoBan ? createBanUuidsChange(requestsToRemove) + : GroupChange.Actions.newBuilder(); for (UUID uuid : requestsToRemove) { actions.addDeleteRequestingMembers(GroupChange.Actions.DeleteRequestingMemberAction @@ -228,8 +232,9 @@ public final class GroupsV2Operations { return actions; } - public GroupChange.Actions.Builder createRemoveMembersChange(final Set membersToRemove) { - GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder(); + public GroupChange.Actions.Builder createRemoveMembersChange(final Set membersToRemove, boolean alsoBan) { + GroupChange.Actions.Builder actions = alsoBan ? createBanUuidsChange(membersToRemove) + : GroupChange.Actions.newBuilder(); for (UUID remove: membersToRemove) { actions.addDeleteMembers(GroupChange.Actions.DeleteMemberAction @@ -241,7 +246,7 @@ public final class GroupsV2Operations { } public GroupChange.Actions.Builder createLeaveAndPromoteMembersToAdmin(UUID self, List membersToMakeAdmin) { - GroupChange.Actions.Builder actions = createRemoveMembersChange(Collections.singleton(self)); + GroupChange.Actions.Builder actions = createRemoveMembersChange(Collections.singleton(self), false); for (UUID member : membersToMakeAdmin) { actions.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction @@ -342,6 +347,26 @@ public final class GroupsV2Operations { .setAnnouncementsOnly(isAnnouncementGroup)); } + public GroupChange.Actions.Builder createBanUuidsChange(Set banUuids) { + GroupChange.Actions.Builder builder = GroupChange.Actions.newBuilder(); + + for (UUID uuid : banUuids) { + builder.addAddBannedMembers(GroupChange.Actions.AddBannedMemberAction.newBuilder().setAdded(BannedMember.newBuilder().setUserId(encryptUuid(uuid)).build())); + } + + return builder; + } + + public GroupChange.Actions.Builder createUnbanUuidsChange(Set banUuids) { + GroupChange.Actions.Builder builder = GroupChange.Actions.newBuilder(); + + for (UUID uuid : banUuids) { + builder.addDeleteBannedMembers(GroupChange.Actions.DeleteBannedMemberAction.newBuilder().setDeletedUserId(encryptUuid(uuid)).build()); + } + + return builder; + } + private Member.Builder member(ProfileKeyCredential credential, Member.Role role) { ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(new SecureRandom(), groupSecretParams, credential); @@ -410,6 +435,7 @@ public final class GroupsV2Operations { List decryptedMembers = new ArrayList<>(membersList.size()); List decryptedPendingMembers = new ArrayList<>(pendingMembersList.size()); List decryptedRequestingMembers = new ArrayList<>(requestingMembersList.size()); + List decryptedBannedMembers = new ArrayList<>(group.getBannedMembersCount()); for (Member member : membersList) { try { @@ -427,6 +453,10 @@ public final class GroupsV2Operations { decryptedRequestingMembers.add(decryptRequestingMember(member)); } + for (BannedMember member : group.getBannedMembersList()) { + decryptedBannedMembers.add(DecryptedBannedMember.newBuilder().setUuid(decryptUuidToByteString(member.getUserId())).build()); + } + return DecryptedGroup.newBuilder() .setTitle(decryptTitle(group.getTitle())) .setDescription(decryptDescription(group.getDescription())) @@ -439,6 +469,7 @@ public final class GroupsV2Operations { .addAllRequestingMembers(decryptedRequestingMembers) .setDisappearingMessagesTimer(DecryptedTimer.newBuilder().setDuration(decryptDisappearingMessagesTimer(group.getDisappearingMessagesTimer()))) .setInviteLinkPassword(group.getInviteLinkPassword()) + .addAllBannedMembers(decryptedBannedMembers) .build(); } @@ -625,6 +656,16 @@ public final class GroupsV2Operations { builder.setNewIsAnnouncementGroup(actions.getModifyAnnouncementsOnly().getAnnouncementsOnly() ? EnabledState.ENABLED : EnabledState.DISABLED); } + // Field 22 + for (GroupChange.Actions.AddBannedMemberAction action : actions.getAddBannedMembersList()) { + builder.addNewBannedMembers(DecryptedBannedMember.newBuilder().setUuid(decryptUuidToByteString(action.getAdded().getUserId())).build()); + } + + // Field 23 + for (GroupChange.Actions.DeleteBannedMemberAction action : actions.getDeleteBannedMembersList()) { + builder.addDeleteBannedMembers(DecryptedBannedMember.newBuilder().setUuid(decryptUuidToByteString(action.getDeletedUserId())).build()); + } + return builder.build(); } diff --git a/libsignal/service/src/main/proto/DecryptedGroups.proto b/libsignal/service/src/main/proto/DecryptedGroups.proto index 7e537b20a9..8a60c6b508 100644 --- a/libsignal/service/src/main/proto/DecryptedGroups.proto +++ b/libsignal/service/src/main/proto/DecryptedGroups.proto @@ -33,6 +33,10 @@ message DecryptedRequestingMember { uint64 timestamp = 4; } +message DecryptedBannedMember { + bytes uuid = 1; +} + message DecryptedPendingMemberRemoval { bytes uuid = 1; bytes uuidCipherText = 2; @@ -62,6 +66,7 @@ message DecryptedGroup { bytes inviteLinkPassword = 10; string description = 11; EnabledState isAnnouncementGroup = 12; + repeated DecryptedBannedMember bannedMembers = 13; } // Decrypted version of message GroupChange.Actions @@ -88,6 +93,8 @@ message DecryptedGroupChange { bytes newInviteLinkPassword = 19; DecryptedString newDescription = 20; EnabledState newIsAnnouncementGroup = 21; + repeated DecryptedBannedMember newBannedMembers = 22; + repeated DecryptedBannedMember deleteBannedMembers = 23; } message DecryptedString { diff --git a/libsignal/service/src/main/proto/Groups.proto b/libsignal/service/src/main/proto/Groups.proto index 4342df4cfc..b4caa53111 100644 --- a/libsignal/service/src/main/proto/Groups.proto +++ b/libsignal/service/src/main/proto/Groups.proto @@ -45,6 +45,10 @@ message RequestingMember { uint64 timestamp = 4; } +message BannedMember { + bytes userId = 1; +} + message AccessControl { enum AccessRequired { UNKNOWN = 0; @@ -72,6 +76,7 @@ message Group { bytes inviteLinkPassword = 10; bytes description = 11; bool announcementsOnly = 12; + repeated BannedMember bannedMembers = 13; } message GroupChange { @@ -121,6 +126,14 @@ message GroupChange { Member.Role role = 2; } + message AddBannedMemberAction { + BannedMember added = 1; + } + + message DeleteBannedMemberAction { + bytes deletedUserId = 1; + } + message ModifyTitleAction { bytes title = 1; } @@ -178,6 +191,8 @@ message GroupChange { ModifyInviteLinkPasswordAction modifyInviteLinkPassword = 19; ModifyDescriptionAction modifyDescription = 20; ModifyAnnouncementsOnlyAction modifyAnnouncementsOnly = 21; + repeated AddBannedMemberAction addBannedMembers = 22; + repeated DeleteBannedMemberAction deleteBannedMembers = 23; } bytes actions = 1; diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_apply_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_apply_Test.java index 4528ad1562..0a1f5bce44 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_apply_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_apply_Test.java @@ -6,6 +6,7 @@ import org.junit.Test; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedApproveMember; +import org.signal.storageservice.protos.groups.local.DecryptedBannedMember; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedMember; @@ -26,6 +27,7 @@ import static org.junit.Assert.assertEquals; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.asAdmin; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.asMember; +import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.bannedMember; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.newProfileKey; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember; @@ -46,7 +48,7 @@ public final class DecryptedGroupUtil_apply_Test { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class); assertEquals("DecryptedGroupUtil and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(), - 21, maxFieldFound); + 23, maxFieldFound); } @Test @@ -885,4 +887,75 @@ public final class DecryptedGroupUtil_apply_Test { newGroup); } + + + @Test + public void apply_new_banned_member() throws NotAbleToApplyGroupV2ChangeException { + DecryptedMember member1 = member(UUID.randomUUID()); + DecryptedBannedMember banned = bannedMember(UUID.randomUUID()); + + DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder() + .setRevision(10) + .addMembers(member1) + .build(), + DecryptedGroupChange.newBuilder() + .setRevision(11) + .addNewBannedMembers(banned) + .build()); + + assertEquals(DecryptedGroup.newBuilder() + .setRevision(11) + .addMembers(member1) + .addBannedMembers(banned) + .build(), + newGroup); + } + + @Test + public void apply_new_banned_member_already_banned() throws NotAbleToApplyGroupV2ChangeException { + DecryptedMember member1 = member(UUID.randomUUID()); + DecryptedBannedMember banned = bannedMember(UUID.randomUUID()); + + DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder() + .setRevision(10) + .addMembers(member1) + .addBannedMembers(banned) + .build(), + DecryptedGroupChange.newBuilder() + .setRevision(11) + .addNewBannedMembers(banned) + .build()); + + assertEquals(DecryptedGroup.newBuilder() + .setRevision(11) + .addMembers(member1) + .addBannedMembers(banned) + .build(), + newGroup); + } + + @Test + public void remove_banned_member() throws NotAbleToApplyGroupV2ChangeException { + DecryptedMember member1 = member(UUID.randomUUID()); + UUID bannedUuid = UUID.randomUUID(); + DecryptedBannedMember banned = bannedMember(bannedUuid); + + DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder() + .setRevision(10) + .addMembers(member1) + .addBannedMembers(banned) + .build(), + DecryptedGroupChange.newBuilder() + .setRevision(11) + .addDeleteBannedMembers(DecryptedBannedMember.newBuilder() + .setUuid(UuidUtil.toByteString(bannedUuid)) + .build()) + .build()); + + assertEquals(DecryptedGroup.newBuilder() + .setRevision(11) + .addMembers(member1) + .build(), + newGroup); + } } diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_empty_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_empty_Test.java index 3261af62e8..e64014ceef 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_empty_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_empty_Test.java @@ -5,6 +5,7 @@ import com.google.protobuf.ByteString; import org.junit.Test; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.local.DecryptedApproveMember; +import org.signal.storageservice.protos.groups.local.DecryptedBannedMember; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember; import org.signal.storageservice.protos.groups.local.DecryptedString; @@ -36,7 +37,7 @@ public final class DecryptedGroupUtil_empty_Test { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class); assertEquals("DecryptedGroupUtil and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(), - 21, maxFieldFound); + 23, maxFieldFound); } @Test @@ -233,4 +234,24 @@ public final class DecryptedGroupUtil_empty_Test { assertFalse(DecryptedGroupUtil.changeIsEmpty(change)); assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change)); } + + @Test + public void not_empty_with_add_banned_member_field_22() { + DecryptedGroupChange change = DecryptedGroupChange.newBuilder() + .addNewBannedMembers(DecryptedBannedMember.getDefaultInstance()) + .build(); + + assertFalse(DecryptedGroupUtil.changeIsEmpty(change)); + assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change)); + } + + @Test + public void not_empty_with_delete_banned_member_field_23() { + DecryptedGroupChange change = DecryptedGroupChange.newBuilder() + .addDeleteBannedMembers(DecryptedBannedMember.getDefaultInstance()) + .build(); + + assertFalse(DecryptedGroupUtil.changeIsEmpty(change)); + assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change)); + } } diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeReconstructTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeReconstructTest.java index cd38960ea1..2f9efe645a 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeReconstructTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeReconstructTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveAdmin; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveMember; +import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.bannedMember; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.demoteAdmin; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.newProfileKey; @@ -42,7 +43,7 @@ public final class GroupChangeReconstructTest { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroup.class); assertEquals("GroupChangeReconstruct and its tests need updating to account for new fields on " + DecryptedGroup.class.getName(), - 12, maxFieldFound); + 13, maxFieldFound); } @Test @@ -381,4 +382,26 @@ public final class GroupChangeReconstructTest { .build(), decryptedGroupChange); } + + @Test + public void new_banned_member() { + UUID uuidNew = UUID.randomUUID(); + DecryptedGroup from = DecryptedGroup.newBuilder().build(); + DecryptedGroup to = DecryptedGroup.newBuilder().addBannedMembers(bannedMember(uuidNew)).build(); + + DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to); + + assertEquals(DecryptedGroupChange.newBuilder().addNewBannedMembers(bannedMember(uuidNew)).build(), decryptedGroupChange); + } + + @Test + public void removed_banned_member() { + UUID uuidOld = UUID.randomUUID(); + DecryptedGroup from = DecryptedGroup.newBuilder().addBannedMembers(bannedMember(uuidOld)).build(); + DecryptedGroup to = DecryptedGroup.newBuilder().build(); + + DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to); + + assertEquals(DecryptedGroupChange.newBuilder().addDeleteBannedMembers(bannedMember(uuidOld)).build(), decryptedGroupChange); + } } \ No newline at end of file diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_changeIsEmpty_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_changeIsEmpty_Test.java index cc43cdd0e2..23914c91d4 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_changeIsEmpty_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_changeIsEmpty_Test.java @@ -20,7 +20,7 @@ public final class GroupChangeUtil_changeIsEmpty_Test { int maxFieldFound = getMaxDeclaredFieldNumber(GroupChange.Actions.class); assertEquals("GroupChangeUtil and its tests need updating to account for new fields on " + GroupChange.Actions.class.getName(), - 21, maxFieldFound); + 23, maxFieldFound); } @Test @@ -198,4 +198,22 @@ public final class GroupChangeUtil_changeIsEmpty_Test { assertFalse(GroupChangeUtil.changeIsEmpty(actions)); } + + @Test + public void not_empty_with_add_banned_member_field_22() { + GroupChange.Actions actions = GroupChange.Actions.newBuilder() + .addAddBannedMembers(GroupChange.Actions.AddBannedMemberAction.getDefaultInstance()) + .build(); + + assertFalse(GroupChangeUtil.changeIsEmpty(actions)); + } + + @Test + public void not_empty_with_delete_banned_member_field_23() { + GroupChange.Actions actions = GroupChange.Actions.newBuilder() + .addDeleteBannedMembers(GroupChange.Actions.DeleteBannedMemberAction.getDefaultInstance()) + .build(); + + assertFalse(GroupChangeUtil.changeIsEmpty(actions)); + } } diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_Test.java index e30fb5a4cb..fc33b6dc74 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_Test.java @@ -4,6 +4,7 @@ import com.google.protobuf.ByteString; import org.junit.Test; import org.signal.storageservice.protos.groups.AccessControl; +import org.signal.storageservice.protos.groups.BannedMember; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.PendingMember; @@ -22,6 +23,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveMember; +import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.bannedMember; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.demoteAdmin; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.encrypt; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.encryptedMember; @@ -47,7 +49,7 @@ public final class GroupChangeUtil_resolveConflict_Test { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class); assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(), - 21, maxFieldFound); + 23, maxFieldFound); } /** @@ -60,7 +62,7 @@ public final class GroupChangeUtil_resolveConflict_Test { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class); assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + GroupChange.class.getName(), - 21, maxFieldFound); + 23, maxFieldFound); } /** @@ -73,7 +75,7 @@ public final class GroupChangeUtil_resolveConflict_Test { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroup.class); assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroup.class.getName(), - 12, maxFieldFound); + 13, maxFieldFound); } @@ -741,4 +743,63 @@ public final class GroupChangeUtil_resolveConflict_Test { assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions)); } + + + @Test + public void field_22__add_banned_members() { + UUID member1 = UUID.randomUUID(); + UUID member2 = UUID.randomUUID(); + UUID member3 = UUID.randomUUID(); + DecryptedGroup groupState = DecryptedGroup.newBuilder() + .addMembers(member(member1)) + .addBannedMembers(bannedMember(member3)) + .build(); + DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder() + .addNewBannedMembers(bannedMember(member1)) + .addNewBannedMembers(bannedMember(member2)) + .addNewBannedMembers(bannedMember(member3)) + .build(); + + GroupChange.Actions change = GroupChange.Actions.newBuilder() + .addAddBannedMembers(GroupChange.Actions.AddBannedMemberAction.newBuilder().setAdded(BannedMember.newBuilder().setUserId(encrypt(member1)))) + .addAddBannedMembers(GroupChange.Actions.AddBannedMemberAction.newBuilder().setAdded(BannedMember.newBuilder().setUserId(encrypt(member2)))) + .addAddBannedMembers(GroupChange.Actions.AddBannedMemberAction.newBuilder().setAdded(BannedMember.newBuilder().setUserId(encrypt(member3)))) + .build(); + + GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build(); + + GroupChange.Actions expected = GroupChange.Actions.newBuilder() + .addAddBannedMembers(GroupChange.Actions.AddBannedMemberAction.newBuilder().setAdded(BannedMember.newBuilder().setUserId(encrypt(member1)))) + .addAddBannedMembers(GroupChange.Actions.AddBannedMemberAction.newBuilder().setAdded(BannedMember.newBuilder().setUserId(encrypt(member2)))) + .build(); + assertEquals(expected, resolvedActions); + } + + @Test + public void field_23__delete_banned_members() { + UUID member1 = UUID.randomUUID(); + UUID member2 = UUID.randomUUID(); + UUID member3 = UUID.randomUUID(); + DecryptedGroup groupState = DecryptedGroup.newBuilder() + .addMembers(member(member1)) + .addBannedMembers(bannedMember(member2)) + .build(); + DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder() + .addDeleteBannedMembers(bannedMember(member1)) + .addDeleteBannedMembers(bannedMember(member2)) + .addDeleteBannedMembers(bannedMember(member3)) + .build(); + GroupChange.Actions change = GroupChange.Actions.newBuilder() + .addDeleteBannedMembers(GroupChange.Actions.DeleteBannedMemberAction.newBuilder().setDeletedUserId(encrypt(member1))) + .addDeleteBannedMembers(GroupChange.Actions.DeleteBannedMemberAction.newBuilder().setDeletedUserId(encrypt(member2))) + .addDeleteBannedMembers(GroupChange.Actions.DeleteBannedMemberAction.newBuilder().setDeletedUserId(encrypt(member3))) + .build(); + + GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build(); + + GroupChange.Actions expected = GroupChange.Actions.newBuilder() + .addDeleteBannedMembers(GroupChange.Actions.DeleteBannedMemberAction.newBuilder().setDeletedUserId(encrypt(member2))) + .build(); + assertEquals(expected, resolvedActions); + } } \ No newline at end of file diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_decryptedOnly_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_decryptedOnly_Test.java index 3006f46654..445872ed64 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_decryptedOnly_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_decryptedOnly_Test.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveMember; +import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.bannedMember; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.demoteAdmin; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember; @@ -40,7 +41,7 @@ public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class); assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(), - 21, maxFieldFound); + 23, maxFieldFound); } /** @@ -53,7 +54,7 @@ public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroup.class); assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroup.class.getName(), - 12, maxFieldFound); + 13, maxFieldFound); } @@ -600,4 +601,53 @@ public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test { assertTrue(DecryptedGroupUtil.changeIsEmpty(resolvedChanges)); } + + @Test + public void field_22__add_banned_members() { + UUID member1 = UUID.randomUUID(); + UUID member2 = UUID.randomUUID(); + UUID member3 = UUID.randomUUID(); + DecryptedGroup groupState = DecryptedGroup.newBuilder() + .addMembers(member(member1)) + .addBannedMembers(bannedMember(member3)) + .build(); + DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder() + .addNewBannedMembers(bannedMember(member1)) + .addNewBannedMembers(bannedMember(member2)) + .addNewBannedMembers(bannedMember(member3)) + .build(); + + DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build(); + + DecryptedGroupChange expected = DecryptedGroupChange.newBuilder() + .addNewBannedMembers(bannedMember(member1)) + .addNewBannedMembers(bannedMember(member2)) + .build(); + + assertEquals(expected, resolvedChanges); + } + + @Test + public void field_23__delete_banned_members() { + UUID member1 = UUID.randomUUID(); + UUID member2 = UUID.randomUUID(); + UUID member3 = UUID.randomUUID(); + DecryptedGroup groupState = DecryptedGroup.newBuilder() + .addMembers(member(member1)) + .addBannedMembers(bannedMember(member2)) + .build(); + DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder() + .addDeleteBannedMembers(bannedMember(member1)) + .addDeleteBannedMembers(bannedMember(member2)) + .addDeleteBannedMembers(bannedMember(member3)) + .build(); + + DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build(); + + DecryptedGroupChange expected = DecryptedGroupChange.newBuilder() + .addDeleteBannedMembers(bannedMember(member2)) + .build(); + + assertEquals(expected, resolvedChanges); + } } \ No newline at end of file diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java index 37a4b6ddf7..f493fcdf8d 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java @@ -9,6 +9,7 @@ import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedApproveMember; +import org.signal.storageservice.protos.groups.local.DecryptedBannedMember; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole; @@ -65,7 +66,7 @@ public final class GroupsV2Operations_decrypt_change_Test { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class); assertEquals("GroupV2Operations#decryptChange and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(), - 21, maxFieldFound); + 23, maxFieldFound); } @Test @@ -102,7 +103,8 @@ public final class GroupsV2Operations_decrypt_change_Test { .setRole(Member.Role.DEFAULT) .setProfileKey(ByteString.copyFrom(profileKey.serialize())) .setJoinedAtRevision(10) - .setUuid(UuidUtil.toByteString(newMember)))); + .setUuid(UuidUtil.toByteString(newMember))) + .addDeleteBannedMembers(DecryptedBannedMember.newBuilder().setUuid(UuidUtil.toByteString(newMember)).build())); } @Test @@ -137,7 +139,8 @@ public final class GroupsV2Operations_decrypt_change_Test { .setRole(Member.Role.DEFAULT) .setProfileKey(ByteString.copyFrom(profileKey.serialize())) .setJoinedAtRevision(10) - .setUuid(UuidUtil.toByteString(newMember)))); + .setUuid(UuidUtil.toByteString(newMember))) + .addDeleteBannedMembers(DecryptedBannedMember.newBuilder().setUuid(UuidUtil.toByteString(newMember)).build())); } @Test(expected = InvalidGroupStateException.class) @@ -156,7 +159,7 @@ public final class GroupsV2Operations_decrypt_change_Test { public void can_decrypt_member_removals_field4() { UUID oldMember = UUID.randomUUID(); - assertDecryption(groupOperations.createRemoveMembersChange(Collections.singleton(oldMember)) + assertDecryption(groupOperations.createRemoveMembersChange(Collections.singleton(oldMember), false) .setRevision(10), DecryptedGroupChange.newBuilder() .setRevision(10) @@ -227,7 +230,8 @@ public final class GroupsV2Operations_decrypt_change_Test { .setAddedByUuid(UuidUtil.toByteString(self)) .setUuidCipherText(groupOperations.encryptUuid(newMember)) .setRole(Member.Role.DEFAULT) - .setUuid(UuidUtil.toByteString(newMember)))); + .setUuid(UuidUtil.toByteString(newMember))) + .addDeleteBannedMembers(DecryptedBannedMember.newBuilder().setUuid(UuidUtil.toByteString(newMember)).build())); } @Test @@ -340,11 +344,12 @@ public final class GroupsV2Operations_decrypt_change_Test { public void can_decrypt_member_requests_refusals_field17() { UUID newRequestingMember = UUID.randomUUID(); - assertDecryption(groupOperations.createRefuseGroupJoinRequest(Collections.singleton(newRequestingMember)) + assertDecryption(groupOperations.createRefuseGroupJoinRequest(Collections.singleton(newRequestingMember), true) .setRevision(10), DecryptedGroupChange.newBuilder() .setRevision(10) - .addDeleteRequestingMembers(UuidUtil.toByteString(newRequestingMember))); + .addDeleteRequestingMembers(UuidUtil.toByteString(newRequestingMember)) + .addNewBannedMembers(DecryptedBannedMember.newBuilder().setUuid(UuidUtil.toByteString(newRequestingMember)).build())); } @Test @@ -387,6 +392,30 @@ public final class GroupsV2Operations_decrypt_change_Test { .setNewIsAnnouncementGroup(EnabledState.ENABLED)); } + @Test + public void can_decrypt_member_bans_field22() { + UUID ban = UUID.randomUUID(); + + assertDecryption(groupOperations.createBanUuidsChange(Collections.singleton(ban)) + .setRevision(13), + DecryptedGroupChange.newBuilder() + .setRevision(13) + .addNewBannedMembers(DecryptedBannedMember.newBuilder() + .setUuid(UuidUtil.toByteString(ban)))); + } + + @Test + public void can_decrypt_banned_member_removals_field23() { + UUID ban = UUID.randomUUID(); + + assertDecryption(groupOperations.createUnbanUuidsChange(Collections.singleton(ban)) + .setRevision(13), + DecryptedGroupChange.newBuilder() + .setRevision(13) + .addDeleteBannedMembers(DecryptedBannedMember.newBuilder() + .setUuid(UuidUtil.toByteString(ban)))); + } + private static ProfileKey newProfileKey() { try { return new ProfileKey(Util.getSecretBytes(32)); diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_group_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_group_Test.java index 59e7ae5f94..9fe59f4b2c 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_group_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_group_Test.java @@ -6,12 +6,14 @@ import com.google.protobuf.InvalidProtocolBufferException; import org.junit.Before; import org.junit.Test; import org.signal.storageservice.protos.groups.AccessControl; +import org.signal.storageservice.protos.groups.BannedMember; import org.signal.storageservice.protos.groups.Group; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.PendingMember; import org.signal.storageservice.protos.groups.RequestingMember; import org.signal.storageservice.protos.groups.local.DecryptedApproveMember; +import org.signal.storageservice.protos.groups.local.DecryptedBannedMember; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedMember; @@ -75,7 +77,7 @@ public final class GroupsV2Operations_decrypt_group_Test { int maxFieldFound = getMaxDeclaredFieldNumber(Group.class); assertEquals("GroupOperations and its tests need updating to account for new fields on " + Group.class.getName(), - 12, maxFieldFound); + 13, maxFieldFound); } @Test @@ -295,6 +297,20 @@ public final class GroupsV2Operations_decrypt_group_Test { assertEquals(EnabledState.ENABLED, decryptedGroup.getIsAnnouncementGroup()); } + @Test + public void decrypt_banned_members_field_13() throws VerificationFailedException, InvalidGroupStateException { + UUID member1 = UUID.randomUUID(); + + Group group = Group.newBuilder() + .addBannedMembers(BannedMember.newBuilder().setUserId(groupOperations.encryptUuid(member1))) + .build(); + + DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group); + + assertEquals(1, decryptedGroup.getBannedMembersCount()); + assertEquals(DecryptedBannedMember.newBuilder().setUuid(UuidUtil.toByteString(member1)).build(), decryptedGroup.getBannedMembers(0)); + } + private ByteString encryptProfileKey(UUID uuid, ProfileKey profileKey) { return ByteString.copyFrom(new ClientZkGroupCipher(groupSecretParams).encryptProfileKey(profileKey, uuid).serialize()); } diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/ProtoTestUtils.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/ProtoTestUtils.java index 042415d9a9..aed382fa30 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/ProtoTestUtils.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/ProtoTestUtils.java @@ -5,6 +5,7 @@ import com.google.protobuf.ByteString; import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.RequestingMember; import org.signal.storageservice.protos.groups.local.DecryptedApproveMember; +import org.signal.storageservice.protos.groups.local.DecryptedBannedMember; import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole; import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; @@ -122,6 +123,12 @@ final class ProtoTestUtils { .build(); } + static DecryptedBannedMember bannedMember(UUID uuid) { + return DecryptedBannedMember.newBuilder() + .setUuid(UuidUtil.toByteString(uuid)) + .build(); + } + static DecryptedApproveMember approveMember(UUID uuid) { return approve(uuid, Member.Role.DEFAULT); }