From: AsamK Date: Sat, 15 May 2021 16:05:07 +0000 (+0200) Subject: Prevent last admin from leaving group X-Git-Tag: v0.8.4~33 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/ea633efc9c113918c84994f75ed72116f708dada?ds=inline Prevent last admin from leaving group --- diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index c95ab9b7..889bafb0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -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.LastGroupAdminException; 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())); } - public Pair> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException { + public Pair> sendQuitGroupMessage( + GroupId groupId, Set groupAdmins + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, InvalidNumberException, LastGroupAdminException { 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; - 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); 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 index 00000000..28c52fad --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/groups/LastGroupAdminException.java @@ -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() + ")"); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 06bfdcda..32c61a84 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -250,7 +250,9 @@ public class GroupV2Helper { return commitChange(groupInfoV2, change); } - public Pair leaveGroup(GroupInfoV2 groupInfoV2) throws IOException { + public Pair leaveGroup( + GroupInfoV2 groupInfoV2, Set membersToMakeAdmin + ) throws IOException { 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())); - } 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 removeMembers( diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 435d1896..6bc30698 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -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.LastGroupAdminException; 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.util.HashSet; +import java.util.Set; 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."); + subparser.addArgument("--admin") + .nargs("*") + .help("Specify one or more members to make a group admin, required if you're currently the only admin."); } @Override @@ -36,13 +43,20 @@ public class QuitGroupCommand implements LocalCommand { throw new UserErrorException("Invalid group id: " + e.getMessage()); } + var groupAdmins = ns.getList("admin"); + 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()); + } 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()); } } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index d9ff5996..3e06d613 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -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.LastGroupAdminException; 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.Set; 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 { - m.sendQuitGroupMessage(group); + m.sendQuitGroupMessage(group, Set.of()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new Error.GroupNotFound(e.getMessage()); - } catch (IOException e) { + } catch (IOException | LastGroupAdminException e) { throw new Error.Failure(e.getMessage()); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); } }