]> nmode's Git Repositories - signal-cli/commitdiff
Prevent last admin from leaving group
authorAsamK <asamk@gmx.de>
Sat, 15 May 2021 16:05:07 +0000 (18:05 +0200)
committerAsamK <asamk@gmx.de>
Sat, 15 May 2021 16:05:07 +0000 (18:05 +0200)
lib/src/main/java/org/asamk/signal/manager/Manager.java
lib/src/main/java/org/asamk/signal/manager/groups/LastGroupAdminException.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
src/main/java/org/asamk/signal/commands/QuitGroupCommand.java
src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java

index c95ab9b7b1ff3c62b3acb71ca2157fb42504a843..889bafb0e02151b0fbcb4d7ce116f2914e170d1f 100644 (file)
@@ -27,6 +27,7 @@ import org.asamk.signal.manager.groups.GroupLinkState;
 import org.asamk.signal.manager.groups.GroupNotFoundException;
 import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.groups.GroupUtils;
 import org.asamk.signal.manager.groups.GroupNotFoundException;
 import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.groups.GroupUtils;
+import org.asamk.signal.manager.groups.LastGroupAdminException;
 import org.asamk.signal.manager.groups.NotAGroupMemberException;
 import org.asamk.signal.manager.helper.GroupV2Helper;
 import org.asamk.signal.manager.helper.PinHelper;
 import org.asamk.signal.manager.groups.NotAGroupMemberException;
 import org.asamk.signal.manager.helper.GroupV2Helper;
 import org.asamk.signal.manager.helper.PinHelper;
@@ -763,7 +764,9 @@ public class Manager implements Closeable {
         return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
     }
 
         return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
     }
 
-    public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException {
+    public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(
+            GroupId groupId, Set<String> groupAdmins
+    ) throws GroupNotFoundException, IOException, NotAGroupMemberException, InvalidNumberException, LastGroupAdminException {
         SignalServiceDataMessage.Builder messageBuilder;
 
         final var g = getGroupForUpdating(groupId);
         SignalServiceDataMessage.Builder messageBuilder;
 
         final var g = getGroupForUpdating(groupId);
@@ -775,7 +778,18 @@ public class Manager implements Closeable {
             account.getGroupStore().updateGroup(groupInfoV1);
         } else {
             final var groupInfoV2 = (GroupInfoV2) g;
             account.getGroupStore().updateGroup(groupInfoV1);
         } else {
             final var groupInfoV2 = (GroupInfoV2) g;
-            final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2);
+            final var currentAdmins = g.getAdminMembers();
+            final var newAdmins = getSignalServiceAddresses(groupAdmins);
+            newAdmins.removeAll(currentAdmins);
+            newAdmins.retainAll(g.getMembers());
+            if (currentAdmins.contains(getSelfRecipientId())
+                    && currentAdmins.size() == 1
+                    && g.getMembers().size() > 1
+                    && newAdmins.size() == 0) {
+                // Last admin can't leave the group, unless she's also the last member
+                throw new LastGroupAdminException(g.getGroupId(), g.getTitle());
+            }
+            final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins);
             groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient);
             messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
             account.getGroupStore().updateGroup(groupInfoV2);
             groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient);
             messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
             account.getGroupStore().updateGroup(groupInfoV2);
diff --git a/lib/src/main/java/org/asamk/signal/manager/groups/LastGroupAdminException.java b/lib/src/main/java/org/asamk/signal/manager/groups/LastGroupAdminException.java
new file mode 100644 (file)
index 0000000..28c52fa
--- /dev/null
@@ -0,0 +1,8 @@
+package org.asamk.signal.manager.groups;
+
+public class LastGroupAdminException extends Exception {
+
+    public LastGroupAdminException(GroupId groupId, String groupName) {
+        super("User is last admin in group: " + groupName + " (" + groupId.toBase64() + ")");
+    }
+}
index 06bfdcdac84c7eb0282f551e98d6de82a8f67cfd..32c61a846df1c05b41685b8448fac7f917f5d4d2 100644 (file)
@@ -250,7 +250,9 @@ public class GroupV2Helper {
         return commitChange(groupInfoV2, change);
     }
 
         return commitChange(groupInfoV2, change);
     }
 
-    public Pair<DecryptedGroup, GroupChange> leaveGroup(GroupInfoV2 groupInfoV2) throws IOException {
+    public Pair<DecryptedGroup, GroupChange> leaveGroup(
+            GroupInfoV2 groupInfoV2, Set<RecipientId> membersToMakeAdmin
+    ) throws IOException {
         var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
         final var selfUuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
                 .getUuid()
         var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
         final var selfUuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
                 .getUuid()
@@ -259,9 +261,15 @@ public class GroupV2Helper {
 
         if (selfPendingMember.isPresent()) {
             return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
 
         if (selfPendingMember.isPresent()) {
             return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
-        } else {
-            return ejectMembers(groupInfoV2, Set.of(selfUuid));
         }
         }
+
+        final var adminUuids = membersToMakeAdmin.stream()
+                .map(addressResolver::resolveSignalServiceAddress)
+                .map(SignalServiceAddress::getUuid)
+                .map(Optional::get)
+                .collect(Collectors.toList());
+        final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
+        return commitChange(groupInfoV2, groupOperations.createLeaveAndPromoteMembersToAdmin(selfUuid, adminUuids));
     }
 
     public Pair<DecryptedGroup, GroupChange> removeMembers(
     }
 
     public Pair<DecryptedGroup, GroupChange> removeMembers(
index 435d1896585e0a50991fa0c59d86731360e0252d..6bc306988f1cc096f3c1b1119814111aca1b46b0 100644 (file)
@@ -11,10 +11,14 @@ import org.asamk.signal.manager.Manager;
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupIdFormatException;
 import org.asamk.signal.manager.groups.GroupNotFoundException;
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupIdFormatException;
 import org.asamk.signal.manager.groups.GroupNotFoundException;
+import org.asamk.signal.manager.groups.LastGroupAdminException;
 import org.asamk.signal.manager.groups.NotAGroupMemberException;
 import org.asamk.signal.util.Util;
 import org.asamk.signal.manager.groups.NotAGroupMemberException;
 import org.asamk.signal.util.Util;
+import org.whispersystems.signalservice.api.util.InvalidNumberException;
 
 import java.io.IOException;
 
 import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
 
 import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResults;
 
 
 import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResults;
 
@@ -23,6 +27,9 @@ public class QuitGroupCommand implements LocalCommand {
     @Override
     public void attachToSubparser(final Subparser subparser) {
         subparser.addArgument("-g", "--group").required(true).help("Specify the recipient group ID.");
     @Override
     public void attachToSubparser(final Subparser subparser) {
         subparser.addArgument("-g", "--group").required(true).help("Specify the recipient group ID.");
+        subparser.addArgument("--admin")
+                .nargs("*")
+                .help("Specify one or more members to make a group admin, required if you're currently the only admin.");
     }
 
     @Override
     }
 
     @Override
@@ -36,13 +43,20 @@ public class QuitGroupCommand implements LocalCommand {
             throw new UserErrorException("Invalid group id: " + e.getMessage());
         }
 
             throw new UserErrorException("Invalid group id: " + e.getMessage());
         }
 
+        var groupAdmins = ns.<String>getList("admin");
+
         try {
         try {
-            final var results = m.sendQuitGroupMessage(groupId);
+            final var results = m.sendQuitGroupMessage(groupId,
+                    groupAdmins == null ? Set.of() : new HashSet<>(groupAdmins));
             handleTimestampAndSendMessageResults(writer, results.first(), results.second());
         } catch (IOException e) {
             throw new IOErrorException("Failed to send message: " + e.getMessage());
         } catch (GroupNotFoundException | NotAGroupMemberException e) {
             throw new UserErrorException("Failed to send to group: " + e.getMessage());
             handleTimestampAndSendMessageResults(writer, results.first(), results.second());
         } catch (IOException e) {
             throw new IOErrorException("Failed to send message: " + e.getMessage());
         } catch (GroupNotFoundException | NotAGroupMemberException e) {
             throw new UserErrorException("Failed to send to group: " + e.getMessage());
+        } catch (InvalidNumberException e) {
+            throw new UserErrorException("Failed to parse admin number: " + e.getMessage());
+        } catch (LastGroupAdminException e) {
+            throw new UserErrorException("You need to specify a new admin with --admin: " + e.getMessage());
         }
     }
 }
         }
     }
 }
index d9ff59966c19441a028ce4b90412178a90abe5e8..3e06d613231a13435f3bb6dc0e12df61b79dccec 100644 (file)
@@ -8,6 +8,7 @@ import org.asamk.signal.manager.NotMasterDeviceException;
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
 import org.asamk.signal.manager.groups.GroupNotFoundException;
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
 import org.asamk.signal.manager.groups.GroupNotFoundException;
+import org.asamk.signal.manager.groups.LastGroupAdminException;
 import org.asamk.signal.manager.groups.NotAGroupMemberException;
 import org.asamk.signal.manager.storage.identities.IdentityInfo;
 import org.asamk.signal.util.ErrorUtils;
 import org.asamk.signal.manager.groups.NotAGroupMemberException;
 import org.asamk.signal.manager.storage.identities.IdentityInfo;
 import org.asamk.signal.util.ErrorUtils;
@@ -24,6 +25,7 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -442,11 +444,13 @@ public class DbusSignalImpl implements Signal {
     public void quitGroup(final byte[] groupId) {
         var group = GroupId.unknownVersion(groupId);
         try {
     public void quitGroup(final byte[] groupId) {
         var group = GroupId.unknownVersion(groupId);
         try {
-            m.sendQuitGroupMessage(group);
+            m.sendQuitGroupMessage(group, Set.of());
         } catch (GroupNotFoundException | NotAGroupMemberException e) {
             throw new Error.GroupNotFound(e.getMessage());
         } catch (GroupNotFoundException | NotAGroupMemberException e) {
             throw new Error.GroupNotFound(e.getMessage());
-        } catch (IOException e) {
+        } catch (IOException | LastGroupAdminException e) {
             throw new Error.Failure(e.getMessage());
             throw new Error.Failure(e.getMessage());
+        } catch (InvalidNumberException e) {
+            throw new Error.InvalidNumber(e.getMessage());
         }
     }
 
         }
     }