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