From 87406e2cdb4cb65a50202792350657457801c235 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 12 Jun 2021 11:33:19 +0200 Subject: [PATCH] Implement --delete flag for quitGroup Closes #638 --- CHANGELOG.md | 1 + .../org/asamk/signal/manager/AvatarStore.java | 4 + .../org/asamk/signal/manager/Manager.java | 74 ++++++++++++------- .../manager/storage/groups/GroupStore.java | 6 +- man/signal-cli.1.adoc | 3 + .../signal/commands/QuitGroupCommand.java | 24 +++++- 6 files changed, 81 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26b0cedb..8dae42af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### Added - Added new parameters to `updateGroup` for group v2 features: `--remove-member`, `--admin`, `--remove-admin`, `--reset-link`, `--link`, `--set-permission-add-member`, `--set-permission-edit-details`, `--expiration` +- Added new `--delete` parameter to `quitGroup`, to delete the local group data ### Fixed - Prevent last admin of a group from leaving the group diff --git a/lib/src/main/java/org/asamk/signal/manager/AvatarStore.java b/lib/src/main/java/org/asamk/signal/manager/AvatarStore.java index 3ed99194..8a1e6172 100644 --- a/lib/src/main/java/org/asamk/signal/manager/AvatarStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/AvatarStore.java @@ -49,6 +49,10 @@ public class AvatarStore { deleteAvatar(getProfileAvatarFile(address)); } + public void deleteGroupAvatar(GroupId groupId) throws IOException { + deleteAvatar(getGroupAvatarFile(groupId)); + } + private StreamDetails retrieveAvatar(final File avatarFile) throws IOException { if (!avatarFile.exists()) { return null; 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 08789d95..55b57828 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -779,35 +779,55 @@ public class Manager implements Closeable { public Pair> sendQuitGroupMessage( GroupId groupId, Set groupAdmins ) throws GroupNotFoundException, IOException, NotAGroupMemberException, InvalidNumberException, LastGroupAdminException { - SignalServiceDataMessage.Builder messageBuilder; + var group = getGroupForUpdating(groupId); + if (group instanceof GroupInfoV1) { + return quitGroupV1((GroupInfoV1) group); + } - final var g = getGroupForUpdating(groupId); - if (g instanceof GroupInfoV1) { - var groupInfoV1 = (GroupInfoV1) g; - var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT).withId(groupId.serialize()).build(); - messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); - groupInfoV1.removeMember(account.getSelfRecipientId()); - account.getGroupStore().updateGroup(groupInfoV1); - } else { - final var groupInfoV2 = (GroupInfoV2) g; - 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); + final var newAdmins = getSignalServiceAddresses(groupAdmins); + try { + return quitGroupV2((GroupInfoV2) group, newAdmins); + } catch (ConflictException e) { + // Detected conflicting update, refreshing group and trying again + group = getGroup(groupId, true); + return quitGroupV2((GroupInfoV2) group, newAdmins); } + } - return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId())); + private Pair> quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException { + var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) + .withId(groupInfoV1.getGroupId().serialize()) + .build(); + + var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); + groupInfoV1.removeMember(account.getSelfRecipientId()); + account.getGroupStore().updateGroup(groupInfoV1); + return sendMessage(messageBuilder, groupInfoV1.getMembersWithout(account.getSelfRecipientId())); + } + + private Pair> quitGroupV2( + final GroupInfoV2 groupInfoV2, final Set newAdmins + ) throws LastGroupAdminException, IOException { + final var currentAdmins = groupInfoV2.getAdminMembers(); + newAdmins.removeAll(currentAdmins); + newAdmins.retainAll(groupInfoV2.getMembers()); + if (currentAdmins.contains(getSelfRecipientId()) + && currentAdmins.size() == 1 + && groupInfoV2.getMembers().size() > 1 + && newAdmins.size() == 0) { + // Last admin can't leave the group, unless she's also the last member + throw new LastGroupAdminException(groupInfoV2.getGroupId(), groupInfoV2.getTitle()); + } + final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins); + groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient); + var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); + account.getGroupStore().updateGroup(groupInfoV2); + return sendMessage(messageBuilder, groupInfoV2.getMembersWithout(account.getSelfRecipientId())); + } + + public void deleteGroup(GroupId groupId) throws IOException { + account.getGroupStore().deleteGroup(groupId); + avatarStore.deleteGroupAvatar(groupId); } public Pair> createGroup( @@ -2013,6 +2033,7 @@ public class Manager implements Closeable { Exception exception = null; final CachedMessage[] cachedMessage = {null}; account.setLastReceiveTimestamp(System.currentTimeMillis()); + logger.debug("Checking for new message from server"); try { var result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> { final var recipientId = envelope1.hasSource() @@ -2021,6 +2042,7 @@ public class Manager implements Closeable { // store message on disk, before acknowledging receipt to the server cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); }); + logger.debug("New message received from server"); if (result.isPresent()) { envelope = result.get(); } else { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java index 19fc7564..dbbebdf0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java @@ -133,7 +133,11 @@ public class GroupStore { saver.save(storage); } - public void deleteGroupV1(GroupIdV1 groupId) { + public void deleteGroupV1(GroupIdV1 groupIdV1) { + deleteGroup(groupIdV1); + } + + public void deleteGroup(GroupId groupId) { final Storage storage; synchronized (groups) { groups.remove(groupId); diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index e73e7cdc..8e011c90 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -285,6 +285,9 @@ If the user is a pending member, this command will decline the group invitation. *-g* GROUP, *--group* GROUP:: Specify the recipient group ID in base64 encoding. +*--delete*:: +Delete local group data completely after quitting group. + === listGroups Show a list of known groups and related information. diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 488c4100..0a53c638 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -1,5 +1,6 @@ package org.asamk.signal.commands; +import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; @@ -14,6 +15,8 @@ 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.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; @@ -24,10 +27,15 @@ import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResu public class QuitGroupCommand implements LocalCommand { + private final static Logger logger = LoggerFactory.getLogger(QuitGroupCommand.class); + @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Send a quit group message to all group members and remove self from member list."); subparser.addArgument("-g", "--group").required(true).help("Specify the recipient group ID."); + subparser.addArgument("--delete") + .action(Arguments.storeTrue()) + .help("Delete local group data completely after quitting group."); subparser.addArgument("--admin") .nargs("*") .help("Specify one or more members to make a group admin, required if you're currently the only admin."); @@ -47,12 +55,20 @@ public class QuitGroupCommand implements LocalCommand { var groupAdmins = ns.getList("admin"); try { - final var results = m.sendQuitGroupMessage(groupId, - groupAdmins == null ? Set.of() : new HashSet<>(groupAdmins)); - handleTimestampAndSendMessageResults(writer, results.first(), results.second()); + try { + final var results = m.sendQuitGroupMessage(groupId, + groupAdmins == null ? Set.of() : new HashSet<>(groupAdmins)); + handleTimestampAndSendMessageResults(writer, results.first(), results.second()); + } catch (NotAGroupMemberException e) { + logger.info("User is not a group member"); + } + if (ns.getBoolean("delete")) { + logger.debug("Deleting group {}", groupId); + m.deleteGroup(groupId); + } } catch (IOException e) { throw new IOErrorException("Failed to send message: " + e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); } catch (InvalidNumberException e) { throw new UserErrorException("Failed to parse admin number: " + e.getMessage()); -- 2.50.1