]> nmode's Git Repositories - signal-cli/blobdiff - lib/src/main/java/org/asamk/signal/manager/Manager.java
Skip invalid contact records
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / Manager.java
index 28e06bbe0ec6c182776c50fba55fa403f35450ea..08789d9581589c5a7ada04be362e8fc17f395361 100644 (file)
@@ -25,7 +25,9 @@ import org.asamk.signal.manager.groups.GroupIdV1;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
 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;
@@ -119,6 +121,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage
 import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
 import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
 import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
 import org.whispersystems.signalservice.api.util.DeviceNameUtil;
@@ -328,6 +331,17 @@ public class Manager implements Closeable {
     }
 
     public void checkAccountState() throws IOException {
+        if (account.getLastReceiveTimestamp() == 0) {
+            logger.warn("The Signal protocol expects that incoming messages are regularly received.");
+        } else {
+            var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp();
+            long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS);
+            if (days > 7) {
+                logger.warn(
+                        "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
+                        days);
+            }
+        }
         if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) {
             refreshPreKeys();
         }
@@ -572,7 +586,7 @@ public class Manager implements Closeable {
     ) {
         var profile = account.getProfileStore().getProfile(recipientId);
 
-        var now = new Date().getTime();
+        var now = System.currentTimeMillis();
         // Profiles are cached for 24h before retrieving them again, unless forced
         if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) {
             return profile;
@@ -598,7 +612,7 @@ public class Manager implements Closeable {
 
         var profileKey = account.getProfileStore().getProfileKey(recipientId);
         if (profileKey == null) {
-            profile = new Profile(new Date().getTime(),
+            profile = new Profile(System.currentTimeMillis(),
                     null,
                     null,
                     null,
@@ -639,7 +653,7 @@ public class Manager implements Closeable {
             }
         } catch (InvalidKeyException ignored) {
             logger.warn("Got invalid identity key in profile for {}",
-                    resolveSignalServiceAddress(recipientId).getLegacyIdentifier());
+                    resolveSignalServiceAddress(recipientId).getIdentifier());
         }
         return profileAndCredential;
     }
@@ -762,7 +776,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);
@@ -774,7 +790,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);
@@ -836,6 +863,8 @@ public class Manager implements Closeable {
             List<String> removeAdmins,
             boolean resetGroupLink,
             GroupLinkState groupLinkState,
+            GroupPermission addMemberPermission,
+            GroupPermission editDetailsPermission,
             File avatarFile,
             Integer expirationTimer
     ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
@@ -848,6 +877,8 @@ public class Manager implements Closeable {
                 removeAdmins == null ? null : getSignalServiceAddresses(removeAdmins),
                 resetGroupLink,
                 groupLinkState,
+                addMemberPermission,
+                editDetailsPermission,
                 avatarFile,
                 expirationTimer);
     }
@@ -862,23 +893,45 @@ public class Manager implements Closeable {
             final Set<RecipientId> removeAdmins,
             final boolean resetGroupLink,
             final GroupLinkState groupLinkState,
+            final GroupPermission addMemberPermission,
+            final GroupPermission editDetailsPermission,
             final File avatarFile,
-            Integer expirationTimer
+            final Integer expirationTimer
     ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
         var group = getGroupForUpdating(groupId);
 
         if (group instanceof GroupInfoV2) {
-            return updateGroupV2((GroupInfoV2) group,
-                    name,
-                    description,
-                    members,
-                    removeMembers,
-                    admins,
-                    removeAdmins,
-                    resetGroupLink,
-                    groupLinkState,
-                    avatarFile,
-                    expirationTimer);
+            try {
+                return updateGroupV2((GroupInfoV2) group,
+                        name,
+                        description,
+                        members,
+                        removeMembers,
+                        admins,
+                        removeAdmins,
+                        resetGroupLink,
+                        groupLinkState,
+                        addMemberPermission,
+                        editDetailsPermission,
+                        avatarFile,
+                        expirationTimer);
+            } catch (ConflictException e) {
+                // Detected conflicting update, refreshing group and trying again
+                group = getGroup(groupId, true);
+                return updateGroupV2((GroupInfoV2) group,
+                        name,
+                        description,
+                        members,
+                        removeMembers,
+                        admins,
+                        removeAdmins,
+                        resetGroupLink,
+                        groupLinkState,
+                        addMemberPermission,
+                        editDetailsPermission,
+                        avatarFile,
+                        expirationTimer);
+            }
         }
 
         final var gv1 = (GroupInfoV1) group;
@@ -948,6 +1001,8 @@ public class Manager implements Closeable {
             final Set<RecipientId> removeAdmins,
             final boolean resetGroupLink,
             final GroupLinkState groupLinkState,
+            final GroupPermission addMemberPermission,
+            final GroupPermission editDetailsPermission,
             final File avatarFile,
             Integer expirationTimer
     ) throws IOException {
@@ -1020,12 +1075,22 @@ public class Manager implements Closeable {
             result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
         }
 
+        if (addMemberPermission != null) {
+            var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission);
+            result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
+        }
+
+        if (editDetailsPermission != null) {
+            var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission);
+            result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
+        }
+
         if (expirationTimer != null) {
             var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer);
             result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
         }
 
-        if (result == null || name != null || description != null || avatarFile != null) {
+        if (name != null || description != null || avatarFile != null) {
             var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile);
             if (avatarFile != null) {
                 avatarStore.storeGroupAvatar(group.getGroupId(),
@@ -1947,6 +2012,7 @@ public class Manager implements Closeable {
             SignalServiceContent content = null;
             Exception exception = null;
             final CachedMessage[] cachedMessage = {null};
+            account.setLastReceiveTimestamp(System.currentTimeMillis());
             try {
                 var result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> {
                     final var recipientId = envelope1.hasSource()
@@ -2174,7 +2240,16 @@ public class Manager implements Closeable {
                         try (var attachmentAsStream = retrieveAttachmentAsStream(groupsMessage.asPointer(), tmpFile)) {
                             var s = new DeviceGroupsInputStream(attachmentAsStream);
                             DeviceGroup g;
-                            while ((g = s.read()) != null) {
+                            while (true) {
+                                try {
+                                    g = s.read();
+                                } catch (IOException e) {
+                                    logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage());
+                                    continue;
+                                }
+                                if (g == null) {
+                                    break;
+                                }
                                 var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId()));
                                 if (syncGroup != null) {
                                     if (g.getName().isPresent()) {
@@ -2245,7 +2320,17 @@ public class Manager implements Closeable {
                                 .asPointer(), tmpFile)) {
                             var s = new DeviceContactsInputStream(attachmentAsStream);
                             DeviceContact c;
-                            while ((c = s.read()) != null) {
+                            while (true) {
+                                try {
+                                    c = s.read();
+                                } catch (IOException e) {
+                                    logger.warn("Sync contacts contained invalid contact, ignoring: {}",
+                                            e.getMessage());
+                                    continue;
+                                }
+                                if (c == null) {
+                                    break;
+                                }
                                 if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
                                     account.setProfileKey(c.getProfileKey().get());
                                 }
@@ -2656,8 +2741,12 @@ public class Manager implements Closeable {
     }
 
     public GroupInfo getGroup(GroupId groupId) {
+        return getGroup(groupId, false);
+    }
+
+    public GroupInfo getGroup(GroupId groupId, boolean forceUpdate) {
         final var group = account.getGroupStore().getGroup(groupId);
-        if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
+        if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) {
             final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
             ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), this::resolveRecipient);
             account.getGroupStore().updateGroup(group);