Prevent last admin from leaving group

This commit is contained in:
AsamK 2021-05-15 18:05:07 +02:00
parent 78f22c7020
commit ea633efc9c
5 changed files with 56 additions and 8 deletions

View 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.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<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);
@ -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);

View file

@ -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() + ")");
}
}

View file

@ -250,7 +250,9 @@ public class GroupV2Helper {
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()
@ -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<DecryptedGroup, GroupChange> removeMembers(

View 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.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.<String>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());
}
}
}

View 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.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());
}
}