]> nmode's Git Repositories - signal-cli/blobdiff - lib/src/main/java/org/asamk/signal/manager/Manager.java
Implement remove group members
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / Manager.java
index 4d519c51c58d08f20729e9f19afa9c0c8c85f7b6..9d73ac814d61d612d1624df98142ff0f787957f2 100644 (file)
@@ -83,6 +83,7 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager;
 import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
 import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
 import org.whispersystems.signalservice.api.SignalServiceMessageSender;
+import org.whispersystems.signalservice.api.SignalSessionLock;
 import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
 import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
@@ -161,6 +162,7 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -193,6 +195,15 @@ public class Manager implements Closeable {
     private final PinHelper pinHelper;
     private final AvatarStore avatarStore;
     private final AttachmentStore attachmentStore;
+    private final SignalSessionLock sessionLock = new SignalSessionLock() {
+        private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
+
+        @Override
+        public Lock acquire() {
+            LEGACY_LOCK.lock();
+            return LEGACY_LOCK::unlock;
+        }
+    };
 
     Manager(
             SignalAccount account,
@@ -389,6 +400,7 @@ public class Manager implements Closeable {
                     newProfile.getInternalServiceName(),
                     newProfile.getAbout() == null ? "" : newProfile.getAbout(),
                     newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
+                    Optional.absent(),
                     streamDetails);
         }
 
@@ -534,6 +546,7 @@ public class Manager implements Closeable {
                 account.getPassword(),
                 account.getDeviceId(),
                 account.getSignalProtocolStore(),
+                sessionLock,
                 userAgent,
                 account.isMultiDevice(),
                 Optional.fromNullable(messagePipe),
@@ -769,95 +782,94 @@ public class Manager implements Closeable {
         return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
     }
 
-    public Pair<GroupId, List<SendMessageResult>> updateGroup(
-            GroupId groupId, String name, List<String> members, File avatarFile
-    ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
-        final var membersRecipientIds = members == null ? null : getSignalServiceAddresses(members);
-        if (membersRecipientIds != null) {
-            membersRecipientIds.remove(account.getSelfRecipientId());
-        }
-        return sendUpdateGroupMessage(groupId, name, membersRecipientIds, avatarFile);
+    public Pair<GroupId, List<SendMessageResult>> createGroup(
+            String name, List<String> members, File avatarFile
+    ) throws IOException, AttachmentInvalidException, InvalidNumberException {
+        return createGroup(name, members == null ? null : getSignalServiceAddresses(members), avatarFile);
     }
 
-    private Pair<GroupId, List<SendMessageResult>> sendUpdateGroupMessage(
-            GroupId groupId, String name, Set<RecipientId> members, File avatarFile
-    ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
-        GroupInfo g;
+    private Pair<GroupId, List<SendMessageResult>> createGroup(
+            String name, Set<RecipientId> members, File avatarFile
+    ) throws IOException, AttachmentInvalidException {
+        final var selfRecipientId = account.getSelfRecipientId();
+        if (members != null && members.contains(selfRecipientId)) {
+            members = new HashSet<>(members);
+            members.remove(selfRecipientId);
+        }
+
+        var gv2Pair = groupHelper.createGroupV2(name == null ? "" : name,
+                members == null ? Set.of() : members,
+                avatarFile);
+
         SignalServiceDataMessage.Builder messageBuilder;
-        if (groupId == null) {
-            // Create new group
-            var gv2Pair = groupHelper.createGroupV2(name == null ? "" : name,
-                    members == null ? Set.of() : members,
-                    avatarFile);
-            if (gv2Pair == null) {
-                var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
-                gv1.addMembers(List.of(account.getSelfRecipientId()));
-                updateGroupV1(gv1, name, members, avatarFile);
-                messageBuilder = getGroupUpdateMessageBuilder(gv1);
-                g = gv1;
-            } else {
-                final var gv2 = gv2Pair.first();
-                final var decryptedGroup = gv2Pair.second();
+        if (gv2Pair == null) {
+            // Failed to create v2 group, creating v1 group instead
+            var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
+            gv1.addMembers(List.of(selfRecipientId));
+            final var result = updateGroupV1(gv1, name, members, avatarFile);
+            return new Pair<>(gv1.getGroupId(), result.second());
+        }
 
-                gv2.setGroup(decryptedGroup, this::resolveRecipient);
-                if (avatarFile != null) {
-                    avatarStore.storeGroupAvatar(gv2.getGroupId(),
-                            outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
-                }
-                messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
-                g = gv2;
-            }
-        } else {
-            var group = getGroupForUpdating(groupId);
-            if (group instanceof GroupInfoV2) {
-                final var groupInfoV2 = (GroupInfoV2) group;
-
-                Pair<Long, List<SendMessageResult>> result = null;
-                if (groupInfoV2.isPendingMember(account.getSelfRecipientId())) {
-                    var groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2);
-                    result = sendUpdateGroupMessage(groupInfoV2,
-                            groupGroupChangePair.first(),
-                            groupGroupChangePair.second());
-                }
+        final var gv2 = gv2Pair.first();
+        final var decryptedGroup = gv2Pair.second();
 
-                if (members != null) {
-                    final var newMembers = new HashSet<>(members);
-                    newMembers.removeAll(group.getMembers());
-                    if (newMembers.size() > 0) {
-                        var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers);
-                        result = sendUpdateGroupMessage(groupInfoV2,
-                                groupGroupChangePair.first(),
-                                groupGroupChangePair.second());
-                    }
-                }
-                if (result == null || name != null || avatarFile != null) {
-                    var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, name, avatarFile);
-                    if (avatarFile != null) {
-                        avatarStore.storeGroupAvatar(groupInfoV2.getGroupId(),
-                                outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
-                    }
-                    result = sendUpdateGroupMessage(groupInfoV2,
-                            groupGroupChangePair.first(),
-                            groupGroupChangePair.second());
-                }
+        gv2.setGroup(decryptedGroup, this::resolveRecipient);
+        if (avatarFile != null) {
+            avatarStore.storeGroupAvatar(gv2.getGroupId(),
+                    outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
+        }
+        messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
+        account.getGroupStore().updateGroup(gv2);
 
-                return new Pair<>(group.getGroupId(), result.second());
-            } else {
-                var gv1 = (GroupInfoV1) group;
-                updateGroupV1(gv1, name, members, avatarFile);
-                messageBuilder = getGroupUpdateMessageBuilder(gv1);
-                g = gv1;
-            }
+        final var result = sendMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId));
+        return new Pair<>(gv2.getGroupId(), result.second());
+    }
+
+    public Pair<Long, List<SendMessageResult>> updateGroup(
+            GroupId groupId,
+            String name,
+            String description,
+            List<String> members,
+            List<String> removeMembers,
+            File avatarFile
+    ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
+        return updateGroup(groupId,
+                name,
+                description,
+                members == null ? null : getSignalServiceAddresses(members),
+                removeMembers == null ? null : getSignalServiceAddresses(removeMembers),
+                avatarFile);
+    }
+
+    private Pair<Long, List<SendMessageResult>> updateGroup(
+            GroupId groupId,
+            String name,
+            String description,
+            Set<RecipientId> members,
+            final Set<RecipientId> removeMembers,
+            File avatarFile
+    ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
+        var group = getGroupForUpdating(groupId);
+
+        if (group instanceof GroupInfoV2) {
+            return updateGroupV2((GroupInfoV2) group, name, description, members, removeMembers, avatarFile);
         }
 
-        account.getGroupStore().updateGroup(g);
+        return updateGroupV1((GroupInfoV1) group, name, members, avatarFile);
+    }
 
-        final var result = sendMessage(messageBuilder,
-                g.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
-        return new Pair<>(g.getGroupId(), result.second());
+    private Pair<Long, List<SendMessageResult>> updateGroupV1(
+            final GroupInfoV1 gv1, final String name, final Set<RecipientId> members, final File avatarFile
+    ) throws IOException, AttachmentInvalidException {
+        updateGroupV1Details(gv1, name, members, avatarFile);
+        var messageBuilder = getGroupUpdateMessageBuilder(gv1);
+
+        account.getGroupStore().updateGroup(gv1);
+
+        return sendMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
     }
 
-    private void updateGroupV1(
+    private void updateGroupV1Details(
             final GroupInfoV1 g, final String name, final Collection<RecipientId> members, final File avatarFile
     ) throws IOException {
         if (name != null) {
@@ -895,13 +907,59 @@ public class Manager implements Closeable {
         }
     }
 
-    public Pair<GroupId, List<SendMessageResult>> joinGroup(
-            GroupInviteLinkUrl inviteLinkUrl
-    ) throws IOException, GroupLinkNotActiveException {
-        return sendJoinGroupMessage(inviteLinkUrl);
+    private Pair<Long, List<SendMessageResult>> updateGroupV2(
+            final GroupInfoV2 group,
+            final String name,
+            final String description,
+            final Set<RecipientId> members,
+            final Set<RecipientId> removeMembers,
+            final File avatarFile
+    ) throws IOException {
+        Pair<Long, List<SendMessageResult>> result = null;
+        if (group.isPendingMember(account.getSelfRecipientId())) {
+            var groupGroupChangePair = groupHelper.acceptInvite(group);
+            result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
+        }
+
+        if (members != null) {
+            final var newMembers = new HashSet<>(members);
+            newMembers.removeAll(group.getMembers());
+            if (newMembers.size() > 0) {
+                var groupGroupChangePair = groupHelper.updateGroupV2(group, newMembers);
+                result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
+            }
+        }
+
+        if (removeMembers != null) {
+            var existingRemoveMembers = new HashSet<>(removeMembers);
+            existingRemoveMembers.retainAll(group.getMembers());
+            existingRemoveMembers.remove(getSelfRecipientId());// self can be removed with sendQuitGroupMessage
+            if (existingRemoveMembers.size() > 0) {
+                var groupGroupChangePair = groupHelper.removeMembers(group, existingRemoveMembers);
+                result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
+            }
+
+            var pendingRemoveMembers = new HashSet<>(removeMembers);
+            pendingRemoveMembers.retainAll(group.getPendingMembers());
+            if (pendingRemoveMembers.size() > 0) {
+                var groupGroupChangePair = groupHelper.revokeInvitedMembers(group, pendingRemoveMembers);
+                result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
+            }
+        }
+
+        if (result == null || name != null || description != null || avatarFile != null) {
+            var groupGroupChangePair = groupHelper.updateGroupV2(group, name, description, avatarFile);
+            if (avatarFile != null) {
+                avatarStore.storeGroupAvatar(group.getGroupId(),
+                        outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
+            }
+            result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
+        }
+
+        return result;
     }
 
-    private Pair<GroupId, List<SendMessageResult>> sendJoinGroupMessage(
+    public Pair<GroupId, List<SendMessageResult>> joinGroup(
             GroupInviteLinkUrl inviteLinkUrl
     ) throws IOException, GroupLinkNotActiveException {
         final var groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(),
@@ -918,11 +976,24 @@ public class Manager implements Closeable {
             return new Pair<>(group.getGroupId(), List.of());
         }
 
-        final var result = sendUpdateGroupMessage(group, group.getGroup(), groupChange);
+        final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange);
 
         return new Pair<>(group.getGroupId(), result.second());
     }
 
+    private Pair<Long, List<SendMessageResult>> sendUpdateGroupV2Message(
+            GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
+    ) throws IOException {
+        final var selfRecipientId = account.getSelfRecipientId();
+        final var members = group.getMembersIncludingPendingWithout(selfRecipientId);
+        group.setGroup(newDecryptedGroup, this::resolveRecipient);
+        members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId));
+
+        final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
+        account.getGroupStore().updateGroup(group);
+        return sendMessage(messageBuilder, members);
+    }
+
     private static int currentTimeDays() {
         return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
     }
@@ -945,15 +1016,6 @@ public class Manager implements Closeable {
         }
     }
 
-    private Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(
-            GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
-    ) throws IOException {
-        group.setGroup(newDecryptedGroup, this::resolveRecipient);
-        final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
-        account.getGroupStore().updateGroup(group);
-        return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
-    }
-
     Pair<Long, List<SendMessageResult>> sendGroupInfoMessage(
             GroupIdV1 groupId, SignalServiceAddress recipient
     ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
@@ -1511,18 +1573,12 @@ public class Manager implements Closeable {
         }
     }
 
-    private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, org.whispersystems.libsignal.UntrustedIdentityException {
+    private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException {
         var cipher = new SignalServiceCipher(account.getSelfAddress(),
                 account.getSignalProtocolStore(),
+                sessionLock,
                 certificateValidator);
-        try {
-            return cipher.decrypt(envelope);
-        } catch (ProtocolUntrustedIdentityException e) {
-            if (e.getCause() instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
-                throw (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause();
-            }
-            throw new AssertionError(e);
-        }
+        return cipher.decrypt(envelope);
     }
 
     private void handleEndSession(RecipientId recipientId) {
@@ -1766,9 +1822,9 @@ public class Manager implements Closeable {
         if (!envelope.isReceipt()) {
             try {
                 content = decryptMessage(envelope);
-            } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
+            } catch (ProtocolUntrustedIdentityException e) {
                 if (!envelope.hasSource()) {
-                    final var identifier = ((org.whispersystems.libsignal.UntrustedIdentityException) e).getName();
+                    final var identifier = e.getSender();
                     final var recipientId = resolveRecipient(identifier);
                     try {
                         account.getMessageCache().replaceSender(cachedMessage, recipientId);
@@ -1889,8 +1945,8 @@ public class Manager implements Closeable {
                 handler.handleMessage(envelope, content, exception);
             }
             if (cachedMessage[0] != null) {
-                if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
-                    final var identifier = ((org.whispersystems.libsignal.UntrustedIdentityException) exception).getName();
+                if (exception instanceof ProtocolUntrustedIdentityException) {
+                    final var identifier = ((ProtocolUntrustedIdentityException) exception).getSender();
                     final var recipientId = resolveRecipient(identifier);
                     queuedActions.add(new RetrieveProfileAction(recipientId));
                     if (!envelope.hasSource()) {