]> nmode's Git Repositories - signal-cli/commitdiff
Refactor group store
authorAsamK <asamk@gmx.de>
Sun, 2 May 2021 14:02:54 +0000 (16:02 +0200)
committerAsamK <asamk@gmx.de>
Mon, 3 May 2021 16:43:45 +0000 (18:43 +0200)
14 files changed:
lib/src/main/java/org/asamk/signal/manager/Manager.java
lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java
lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java
lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
lib/src/main/java/org/asamk/signal/manager/storage/Utils.java
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java [deleted file]
src/main/java/org/asamk/signal/commands/JoinGroupCommand.java
src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java

index d55362dec1a7186468ab591264d209f9ef09bd53..3c2d130d3d378e57ad0a1642cd7c86cd5e488b8d 100644 (file)
@@ -265,6 +265,10 @@ public class Manager implements Closeable {
         return account.getSelfAddress();
     }
 
+    public RecipientId getSelfRecipientId() {
+        return account.getSelfRecipientId();
+    }
+
     private IdentityKeyPair getIdentityKeyPair() {
         return account.getIdentityKeyPair();
     }
@@ -673,7 +677,7 @@ public class Manager implements Closeable {
         if (g == null) {
             throw new GroupNotFoundException(groupId);
         }
-        if (!g.isMember(account.getSelfAddress())) {
+        if (!g.isMember(account.getSelfRecipientId())) {
             throw new NotAGroupMemberException(groupId, g.getTitle());
         }
         return g;
@@ -684,7 +688,7 @@ public class Manager implements Closeable {
         if (g == null) {
             throw new GroupNotFoundException(groupId);
         }
-        if (!g.isMember(account.getSelfAddress()) && !g.isPendingMember(account.getSelfAddress())) {
+        if (!g.isMember(account.getSelfRecipientId()) && !g.isPendingMember(account.getSelfRecipientId())) {
             throw new NotAGroupMemberException(groupId, g.getTitle());
         }
         return g;
@@ -725,7 +729,7 @@ public class Manager implements Closeable {
         GroupUtils.setGroupContext(messageBuilder, g);
         messageBuilder.withExpiration(g.getMessageExpirationTime());
 
-        return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
+        return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
     }
 
     public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException {
@@ -736,17 +740,17 @@ public class Manager implements Closeable {
             var groupInfoV1 = (GroupInfoV1) g;
             var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT).withId(groupId.serialize()).build();
             messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
-            groupInfoV1.removeMember(account.getSelfAddress());
+            groupInfoV1.removeMember(account.getSelfRecipientId());
             account.getGroupStore().updateGroup(groupInfoV1);
         } else {
             final var groupInfoV2 = (GroupInfoV2) g;
             final var groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2);
-            groupInfoV2.setGroup(groupGroupChangePair.first());
+            groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient);
             messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
             account.getGroupStore().updateGroup(groupInfoV2);
         }
 
-        return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
+        return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
     }
 
     public Pair<GroupId, List<SendMessageResult>> updateGroup(
@@ -754,11 +758,7 @@ public class Manager implements Closeable {
     ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
         return sendUpdateGroupMessage(groupId,
                 name,
-                members == null
-                        ? null
-                        : getSignalServiceAddresses(members).stream()
-                                .map(this::resolveRecipient)
-                                .collect(Collectors.toSet()),
+                members == null ? null : getSignalServiceAddresses(members),
                 avatarFile);
     }
 
@@ -769,16 +769,20 @@ public class Manager implements Closeable {
         SignalServiceDataMessage.Builder messageBuilder;
         if (groupId == null) {
             // Create new group
-            var gv2 = groupHelper.createGroupV2(name == null ? "" : name,
+            var gv2Pair = groupHelper.createGroupV2(name == null ? "" : name,
                     members == null ? Set.of() : members,
                     avatarFile);
-            if (gv2 == null) {
+            if (gv2Pair == null) {
                 var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
-                gv1.addMembers(List.of(account.getSelfAddress()));
+                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();
+
+                gv2.setGroup(decryptedGroup, this::resolveRecipient);
                 if (avatarFile != null) {
                     avatarStore.storeGroupAvatar(gv2.getGroupId(),
                             outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
@@ -792,7 +796,7 @@ public class Manager implements Closeable {
                 final var groupInfoV2 = (GroupInfoV2) group;
 
                 Pair<Long, List<SendMessageResult>> result = null;
-                if (groupInfoV2.isPendingMember(getSelfAddress())) {
+                if (groupInfoV2.isPendingMember(account.getSelfRecipientId())) {
                     var groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2);
                     result = sendUpdateGroupMessage(groupInfoV2,
                             groupGroupChangePair.first(),
@@ -801,10 +805,7 @@ public class Manager implements Closeable {
 
                 if (members != null) {
                     final var newMembers = new HashSet<>(members);
-                    newMembers.removeAll(group.getMembers()
-                            .stream()
-                            .map(this::resolveRecipient)
-                            .collect(Collectors.toSet()));
+                    newMembers.removeAll(group.getMembers());
                     if (newMembers.size() > 0) {
                         var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers);
                         result = sendUpdateGroupMessage(groupInfoV2,
@@ -834,7 +835,8 @@ public class Manager implements Closeable {
 
         account.getGroupStore().updateGroup(g);
 
-        final var result = sendMessage(messageBuilder, g.getMembersIncludingPendingWithout(account.getSelfAddress()));
+        final var result = sendMessage(messageBuilder,
+                g.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
         return new Pair<>(g.getGroupId(), result.second());
     }
 
@@ -846,12 +848,13 @@ public class Manager implements Closeable {
         }
 
         if (members != null) {
-            final var memberAddresses = members.stream()
+            final var newMemberAddresses = members.stream()
+                    .filter(member -> !g.isMember(member))
                     .map(this::resolveSignalServiceAddress)
                     .collect(Collectors.toList());
             final var newE164Members = new HashSet<String>();
-            for (var member : memberAddresses) {
-                if (g.isMember(member) || !member.getNumber().isPresent()) {
+            for (var member : newMemberAddresses) {
+                if (!member.getNumber().isPresent()) {
                     continue;
                 }
                 newE164Members.add(member.getNumber().get());
@@ -866,7 +869,7 @@ public class Manager implements Closeable {
                         + " to group: Not registered on Signal");
             }
 
-            g.addMembers(memberAddresses);
+            g.addMembers(members);
         }
 
         if (avatarFile != null) {
@@ -928,10 +931,10 @@ public class Manager implements Closeable {
     private Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(
             GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
     ) throws IOException {
-        group.setGroup(newDecryptedGroup);
+        group.setGroup(newDecryptedGroup, this::resolveRecipient);
         final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
         account.getGroupStore().updateGroup(group);
-        return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfAddress()));
+        return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
     }
 
     Pair<Long, List<SendMessageResult>> sendGroupInfoMessage(
@@ -944,21 +947,24 @@ public class Manager implements Closeable {
         }
         g = (GroupInfoV1) group;
 
-        if (!g.isMember(recipient)) {
+        if (!g.isMember(resolveRecipient(recipient))) {
             throw new NotAGroupMemberException(groupId, g.name);
         }
 
         var messageBuilder = getGroupUpdateMessageBuilder(g);
 
         // Send group message only to the recipient who requested it
-        return sendMessage(messageBuilder, List.of(recipient));
+        return sendMessage(messageBuilder, Set.of(resolveRecipient(recipient)));
     }
 
     private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException {
         var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
                 .withId(g.getGroupId().serialize())
                 .withName(g.name)
-                .withMembers(new ArrayList<>(g.getMembers()));
+                .withMembers(g.getMembers()
+                        .stream()
+                        .map(this::resolveSignalServiceAddress)
+                        .collect(Collectors.toList()));
 
         try {
             final var attachment = createGroupAvatarAttachment(g.getGroupId());
@@ -991,7 +997,7 @@ public class Manager implements Closeable {
         var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
 
         // Send group info request message to the recipient who sent us a message with this groupId
-        return sendMessage(messageBuilder, List.of(recipient));
+        return sendMessage(messageBuilder, Set.of(resolveRecipient(recipient)));
     }
 
     void sendReceipt(
@@ -1126,9 +1132,9 @@ public class Manager implements Closeable {
                 .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build());
     }
 
-    private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException {
+    private void sendExpirationTimerUpdate(RecipientId recipientId) throws IOException {
         final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
-        sendMessage(messageBuilder, List.of(address));
+        sendMessage(messageBuilder, Set.of(recipientId));
     }
 
     /**
@@ -1139,7 +1145,7 @@ public class Manager implements Closeable {
     ) throws IOException, InvalidNumberException {
         var recipientId = canonicalizeAndResolveRecipient(number);
         setExpirationTimer(recipientId, messageExpirationTimer);
-        sendExpirationTimerUpdate(resolveSignalServiceAddress(recipientId));
+        sendExpirationTimerUpdate(recipientId);
         account.save();
     }
 
@@ -1266,7 +1272,7 @@ public class Manager implements Closeable {
         messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync());
     }
 
-    private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
+    private Set<RecipientId> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
         final var signalServiceAddresses = new HashSet<SignalServiceAddress>(numbers.size());
         final var addressesMissingUuid = new HashSet<SignalServiceAddress>();
 
@@ -1302,7 +1308,7 @@ public class Manager implements Closeable {
             }
         }
 
-        return signalServiceAddresses;
+        return signalServiceAddresses.stream().map(this::resolveRecipient).collect(Collectors.toSet());
     }
 
     private Map<String, UUID> getRegisteredUsers(final Set<String> numbersMissingUuid) throws IOException {
@@ -1316,10 +1322,8 @@ public class Manager implements Closeable {
     }
 
     private Pair<Long, List<SendMessageResult>> sendMessage(
-            SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients
+            SignalServiceDataMessage.Builder messageBuilder, Set<RecipientId> recipientIds
     ) throws IOException {
-        recipients = recipients.stream().map(this::resolveSignalServiceAddress).collect(Collectors.toSet());
-        final var recipientIds = recipients.stream().map(this::resolveRecipient).collect(Collectors.toSet());
         final var timestamp = System.currentTimeMillis();
         messageBuilder.withTimestamp(timestamp);
         getOrCreateMessagePipe();
@@ -1331,8 +1335,12 @@ public class Manager implements Closeable {
                 try {
                     var messageSender = createMessageSender();
                     final var isRecipientUpdate = false;
-                    var result = messageSender.sendMessage(new ArrayList<>(recipients),
-                            unidentifiedAccessHelper.getAccessFor(recipientIds),
+                    final var recipientIdList = new ArrayList<>(recipientIds);
+                    final var addresses = recipientIdList.stream()
+                            .map(this::resolveSignalServiceAddress)
+                            .collect(Collectors.toList());
+                    var result = messageSender.sendMessage(addresses,
+                            unidentifiedAccessHelper.getAccessFor(recipientIdList),
                             isRecipientUpdate,
                             message);
 
@@ -1352,19 +1360,19 @@ public class Manager implements Closeable {
             } else {
                 // Send to all individually, so sync messages are sent correctly
                 messageBuilder.withProfileKey(account.getProfileKey().serialize());
-                var results = new ArrayList<SendMessageResult>(recipients.size());
-                for (var address : recipients) {
-                    final var contact = account.getContactStore().getContact(resolveRecipient(address));
+                var results = new ArrayList<SendMessageResult>(recipientIds.size());
+                for (var recipientId : recipientIds) {
+                    final var contact = account.getContactStore().getContact(recipientId);
                     final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
                     messageBuilder.withExpiration(expirationTime);
                     message = messageBuilder.build();
-                    results.add(sendMessage(address, message));
+                    results.add(sendMessage(resolveSignalServiceAddress(recipientId), message));
                 }
                 return new Pair<>(timestamp, results);
             }
         } finally {
             if (message != null && message.isEndSession()) {
-                for (var recipient : recipients) {
+                for (var recipient : recipientIds) {
                     handleEndSession(recipient);
                 }
             }
@@ -1448,8 +1456,8 @@ public class Manager implements Closeable {
         }
     }
 
-    private void handleEndSession(SignalServiceAddress source) {
-        account.getSessionStore().deleteAllSessions(source.getIdentifier());
+    private void handleEndSession(RecipientId recipientId) {
+        account.getSessionStore().deleteAllSessions(recipientId);
     }
 
     private List<HandleAction> handleSignalServiceDataMessage(
@@ -1486,7 +1494,7 @@ public class Manager implements Closeable {
                                 groupV1.addMembers(groupInfo.getMembers()
                                         .get()
                                         .stream()
-                                        .map(this::resolveSignalServiceAddress)
+                                        .map(this::resolveRecipient)
                                         .collect(Collectors.toSet()));
                             }
 
@@ -1500,7 +1508,7 @@ public class Manager implements Closeable {
                             break;
                         case QUIT: {
                             if (groupV1 != null) {
-                                groupV1.removeMember(source);
+                                groupV1.removeMember(resolveRecipient(source));
                                 account.getGroupStore().updateGroup(groupV1);
                             }
                             break;
@@ -1527,7 +1535,7 @@ public class Manager implements Closeable {
 
         final var conversationPartnerAddress = isSync ? destination : source;
         if (conversationPartnerAddress != null && message.isEndSession()) {
-            handleEndSession(conversationPartnerAddress);
+            handleEndSession(resolveRecipient(conversationPartnerAddress));
         }
         if (message.isExpirationUpdate() || message.getBody().isPresent()) {
             if (message.getGroupContext().isPresent()) {
@@ -1613,7 +1621,7 @@ public class Manager implements Closeable {
         final GroupInfoV2 groupInfoV2;
         if (groupInfo instanceof GroupInfoV1) {
             // Received a v2 group message for a v1 group, we need to locally migrate the group
-            account.getGroupStore().deleteGroup(groupInfo.getGroupId());
+            account.getGroupStore().deleteGroupV1(((GroupInfoV1) groupInfo).getGroupId());
             groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
             logger.info("Locally migrated group {} to group v2, id: {}",
                     groupInfo.getGroupId().toBase64(),
@@ -1641,7 +1649,7 @@ public class Manager implements Closeable {
                     downloadGroupAvatar(groupId, groupSecretParams, avatar);
                 }
             }
-            groupInfoV2.setGroup(group);
+            groupInfoV2.setGroup(group, this::resolveRecipient);
             account.getGroupStore().updateGroup(groupInfoV2);
         }
 
@@ -1885,7 +1893,7 @@ public class Manager implements Closeable {
                 }
                 var groupId = GroupUtils.getGroupId(message.getGroupContext().get());
                 var group = getGroup(groupId);
-                if (group != null && !group.isMember(source)) {
+                if (group != null && !group.isMember(resolveRecipient(source))) {
                     return true;
                 }
             }
@@ -1959,13 +1967,13 @@ public class Manager implements Closeable {
                                     }
                                     syncGroup.addMembers(g.getMembers()
                                             .stream()
-                                            .map(this::resolveSignalServiceAddress)
+                                            .map(this::resolveRecipient)
                                             .collect(Collectors.toSet()));
                                     if (!g.isActive()) {
-                                        syncGroup.removeMember(account.getSelfAddress());
+                                        syncGroup.removeMember(account.getSelfRecipientId());
                                     } else {
                                         // Add ourself to the member set as it's marked as active
-                                        syncGroup.addMembers(List.of(account.getSelfAddress()));
+                                        syncGroup.addMembers(List.of(account.getSelfRecipientId()));
                                     }
                                     syncGroup.blocked = g.isBlocked();
                                     if (g.getColor().isPresent()) {
@@ -1975,7 +1983,6 @@ public class Manager implements Closeable {
                                     if (g.getAvatar().isPresent()) {
                                         downloadGroupAvatar(g.getAvatar().get(), syncGroup.getGroupId());
                                     }
-                                    syncGroup.inboxPosition = g.getInboxPosition().orNull();
                                     syncGroup.archived = g.isArchived();
                                     account.getGroupStore().updateGroup(syncGroup);
                                 }
@@ -2282,13 +2289,16 @@ public class Manager implements Closeable {
                         var groupInfo = (GroupInfoV1) record;
                         out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
                                 Optional.fromNullable(groupInfo.name),
-                                new ArrayList<>(groupInfo.getMembers()),
+                                groupInfo.getMembers()
+                                        .stream()
+                                        .map(this::resolveSignalServiceAddress)
+                                        .collect(Collectors.toList()),
                                 createGroupAvatarAttachment(groupInfo.getGroupId()),
-                                groupInfo.isMember(account.getSelfAddress()),
+                                groupInfo.isMember(account.getSelfRecipientId()),
                                 Optional.of(groupInfo.messageExpirationTime),
                                 Optional.fromNullable(groupInfo.color),
                                 groupInfo.blocked,
-                                Optional.fromNullable(groupInfo.inboxPosition),
+                                Optional.absent(),
                                 groupInfo.archived));
                     }
                 }
@@ -2434,7 +2444,7 @@ public class Manager implements Closeable {
         final var group = account.getGroupStore().getGroup(groupId);
         if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
             final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
-            ((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams));
+            ((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams), this::resolveRecipient);
             account.getGroupStore().updateGroup(group);
         }
         return group;
index 237a34b6f8fa374b9615330cf3ac77c6f25eb506..d2012fa0fec525b4e58454a6838c0d9908edb21c 100644 (file)
@@ -1,5 +1,7 @@
 package org.asamk.signal.manager.groups;
 
+import java.util.Base64;
+
 import static org.asamk.signal.manager.util.KeyUtils.getSecretBytes;
 
 public class GroupIdV1 extends GroupId {
@@ -8,6 +10,10 @@ public class GroupIdV1 extends GroupId {
         return new GroupIdV1(getSecretBytes(16));
     }
 
+    public static GroupIdV1 fromBase64(String groupId) {
+        return new GroupIdV1(Base64.getDecoder().decode(groupId));
+    }
+
     public GroupIdV1(final byte[] id) {
         super(id);
     }
index 849314da75ff47a0d2fe9297fbea6dcc6bb41544..6abdff078a49544e54305a921b0cf18fddeab7f8 100644 (file)
@@ -100,7 +100,7 @@ public class GroupHelper {
                 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
     }
 
-    public GroupInfoV2 createGroupV2(
+    public Pair<GroupInfoV2, DecryptedGroup> createGroupV2(
             String name, Set<RecipientId> members, File avatarFile
     ) throws IOException {
         final var avatarBytes = readAvatarBytes(avatarFile);
@@ -129,9 +129,8 @@ public class GroupHelper {
         final var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
         final var masterKey = groupSecretParams.getMasterKey();
         var g = new GroupInfoV2(groupId, masterKey);
-        g.setGroup(decryptedGroup);
 
-        return g;
+        return new Pair<>(g, decryptedGroup);
     }
 
     private byte[] readAvatarBytes(final File avatarFile) throws IOException {
index 0b82fb18ef03b83b041bc08c202f6b5e658651b7..87e23c1b33aea4782536d3be4840f6402afb2fba 100644 (file)
@@ -6,7 +6,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
 
-import java.util.Collection;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -76,7 +75,7 @@ public class UnidentifiedAccessHelper {
         }
     }
 
-    public List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<RecipientId> recipients) {
+    public List<Optional<UnidentifiedAccessPair>> getAccessFor(List<RecipientId> recipients) {
         return recipients.stream().map(this::getAccessFor).collect(Collectors.toList());
     }
 
index 0c5290b954e15dc703174b9e5f820838220b1591..f2d2d91d383f18eb9c2e8fbcbf826d273210e27d 100644 (file)
@@ -7,7 +7,7 @@ import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.storage.contacts.ContactsStore;
 import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
-import org.asamk.signal.manager.storage.groups.JsonGroupStore;
+import org.asamk.signal.manager.storage.groups.GroupStore;
 import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
 import org.asamk.signal.manager.storage.messageCache.MessageCache;
 import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
@@ -86,7 +86,8 @@ public class SignalAccount implements Closeable {
     private SignedPreKeyStore signedPreKeyStore;
     private SessionStore sessionStore;
     private IdentityKeyStore identityKeyStore;
-    private JsonGroupStore groupStore;
+    private GroupStore groupStore;
+    private GroupStore.Storage groupStoreStorage;
     private RecipientStore recipientStore;
     private StickerStore stickerStore;
     private StickerStore.Storage stickerStoreStorage;
@@ -130,7 +131,9 @@ public class SignalAccount implements Closeable {
         account.profileKey = profileKey;
 
         account.initStores(dataPath, identityKey, registrationId);
-        account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
+        account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
+                account.recipientStore::resolveRecipient,
+                account::saveGroupStore);
         account.stickerStore = new StickerStore(account::saveStickerStore);
 
         account.registered = false;
@@ -183,7 +186,9 @@ public class SignalAccount implements Closeable {
         account.deviceId = deviceId;
 
         account.initStores(dataPath, identityKey, registrationId);
-        account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
+        account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
+                account.recipientStore::resolveRecipient,
+                account::saveGroupStore);
         account.stickerStore = new StickerStore(account::saveStickerStore);
 
         account.registered = true;
@@ -209,6 +214,7 @@ public class SignalAccount implements Closeable {
         sessionStore.mergeRecipients(recipientId, toBeMergedRecipientId);
         identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
         messageCache.mergeRecipients(recipientId, toBeMergedRecipientId);
+        groupStore.mergeRecipients(recipientId, toBeMergedRecipientId);
     }
 
     public static File getFileName(File dataPath, String username) {
@@ -331,13 +337,16 @@ public class SignalAccount implements Closeable {
 
         loadLegacyStores(rootNode, legacySignalProtocolStore);
 
-        var groupStoreNode = rootNode.get("groupStore");
-        if (groupStoreNode != null) {
-            groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
-            groupStore.groupCachePath = getGroupCachePath(dataPath, username);
-        }
-        if (groupStore == null) {
-            groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
+        if (rootNode.hasNonNull("groupStore")) {
+            groupStoreStorage = jsonProcessor.convertValue(rootNode.get("groupStore"), GroupStore.Storage.class);
+            groupStore = GroupStore.fromStorage(groupStoreStorage,
+                    getGroupCachePath(dataPath, username),
+                    recipientStore::resolveRecipient,
+                    this::saveGroupStore);
+        } else {
+            groupStore = new GroupStore(getGroupCachePath(dataPath, username),
+                    recipientStore::resolveRecipient,
+                    this::saveGroupStore);
         }
 
         if (rootNode.hasNonNull("stickerStore")) {
@@ -510,6 +519,11 @@ public class SignalAccount implements Closeable {
         save();
     }
 
+    private void saveGroupStore(GroupStore.Storage storage) {
+        this.groupStoreStorage = storage;
+        save();
+    }
+
     public void save() {
         synchronized (fileChannel) {
             var rootNode = jsonProcessor.createObjectNode();
@@ -534,7 +548,7 @@ public class SignalAccount implements Closeable {
                     .put("nextSignedPreKeyId", nextSignedPreKeyId)
                     .put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
                     .put("registered", registered)
-                    .putPOJO("groupStore", groupStore)
+                    .putPOJO("groupStore", groupStoreStorage)
                     .putPOJO("stickerStore", stickerStoreStorage);
             try {
                 try (var output = new ByteArrayOutputStream()) {
@@ -597,7 +611,7 @@ public class SignalAccount implements Closeable {
         return identityKeyStore;
     }
 
-    public JsonGroupStore getGroupStore() {
+    public GroupStore getGroupStore() {
         return groupStore;
     }
 
index 51bc2fdf50ec95962534d28363172b838831f9f2..e542f4e33e949df98900bb30b7f1a7bb25bdc978 100644 (file)
@@ -17,15 +17,15 @@ public class Utils {
     }
 
     public static ObjectMapper createStorageObjectMapper() {
-        final ObjectMapper jsonProcessor = new ObjectMapper();
+        final ObjectMapper objectMapper = new ObjectMapper();
 
-        jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY);
-        jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print
-        jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
-        jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
-        jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
+        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY);
+        objectMapper.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print
+        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+        objectMapper.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
+        objectMapper.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
 
-        return jsonProcessor;
+        return objectMapper;
     }
 
     public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
index 68af2b808c83078bdbfbd9cba43c5e21a0b0f46b..8f9146de8df5c0374580634fe0e1decd9dd9b5b1 100644 (file)
@@ -1,10 +1,8 @@
 package org.asamk.signal.manager.storage.groups;
 
-import com.fasterxml.jackson.annotation.JsonIgnore;
-
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
 
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -12,66 +10,43 @@ import java.util.stream.Stream;
 
 public abstract class GroupInfo {
 
-    @JsonIgnore
     public abstract GroupId getGroupId();
 
-    @JsonIgnore
     public abstract String getTitle();
 
-    @JsonIgnore
     public abstract GroupInviteLinkUrl getGroupInviteLink();
 
-    @JsonIgnore
-    public abstract Set<SignalServiceAddress> getMembers();
+    public abstract Set<RecipientId> getMembers();
 
-    @JsonIgnore
-    public Set<SignalServiceAddress> getPendingMembers() {
+    public Set<RecipientId> getPendingMembers() {
         return Set.of();
     }
 
-    @JsonIgnore
-    public Set<SignalServiceAddress> getRequestingMembers() {
+    public Set<RecipientId> getRequestingMembers() {
         return Set.of();
     }
 
-    @JsonIgnore
     public abstract boolean isBlocked();
 
-    @JsonIgnore
     public abstract void setBlocked(boolean blocked);
 
-    @JsonIgnore
     public abstract int getMessageExpirationTime();
 
-    @JsonIgnore
-    public Set<SignalServiceAddress> getMembersWithout(SignalServiceAddress address) {
-        return getMembers().stream().filter(member -> !member.matches(address)).collect(Collectors.toSet());
+    public Set<RecipientId> getMembersWithout(RecipientId recipientId) {
+        return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet());
     }
 
-    @JsonIgnore
-    public Set<SignalServiceAddress> getMembersIncludingPendingWithout(SignalServiceAddress address) {
+    public Set<RecipientId> getMembersIncludingPendingWithout(RecipientId recipientId) {
         return Stream.concat(getMembers().stream(), getPendingMembers().stream())
-                .filter(member -> !member.matches(address))
+                .filter(member -> !member.equals(recipientId))
                 .collect(Collectors.toSet());
     }
 
-    @JsonIgnore
-    public boolean isMember(SignalServiceAddress address) {
-        for (var member : getMembers()) {
-            if (member.matches(address)) {
-                return true;
-            }
-        }
-        return false;
+    public boolean isMember(RecipientId recipientId) {
+        return getMembers().contains(recipientId);
     }
 
-    @JsonIgnore
-    public boolean isPendingMember(SignalServiceAddress address) {
-        for (var member : getPendingMembers()) {
-            if (member.matches(address)) {
-                return true;
-            }
-        }
-        return false;
+    public boolean isPendingMember(RecipientId recipientId) {
+        return getPendingMembers().contains(recipientId);
     }
 }
index 970ec5c39ddc0a384ed05cdd68d5bf0ea4213925..8cf6cb94773f999d56b18d7752c0480269edc83c 100644 (file)
@@ -1,55 +1,27 @@
 package org.asamk.signal.manager.storage.groups;
 
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.JsonSerializer;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializerProvider;
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
-import com.fasterxml.jackson.databind.annotation.JsonSerialize;
-
-import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupIdV1;
 import org.asamk.signal.manager.groups.GroupIdV2;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
 import org.asamk.signal.manager.groups.GroupUtils;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
 
-import java.io.IOException;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.UUID;
 
 public class GroupInfoV1 extends GroupInfo {
 
-    private static final ObjectMapper jsonProcessor = new ObjectMapper();
-
     private final GroupIdV1 groupId;
 
     private GroupIdV2 expectedV2Id;
 
-    @JsonProperty
     public String name;
 
-    @JsonProperty
-    @JsonDeserialize(using = MembersDeserializer.class)
-    @JsonSerialize(using = MembersSerializer.class)
-    public Set<SignalServiceAddress> members = new HashSet<>();
-    @JsonProperty
+    public Set<RecipientId> members = new HashSet<>();
     public String color;
-    @JsonProperty(defaultValue = "0")
     public int messageExpirationTime;
-    @JsonProperty(defaultValue = "false")
     public boolean blocked;
-    @JsonProperty
-    public Integer inboxPosition;
-    @JsonProperty(defaultValue = "false")
     public boolean archived;
 
     public GroupInfoV1(GroupIdV1 groupId) {
@@ -57,41 +29,30 @@ public class GroupInfoV1 extends GroupInfo {
     }
 
     public GroupInfoV1(
-            @JsonProperty("groupId") byte[] groupId,
-            @JsonProperty("expectedV2Id") byte[] expectedV2Id,
-            @JsonProperty("name") String name,
-            @JsonProperty("members") Collection<SignalServiceAddress> members,
-            @JsonProperty("avatarId") long _ignored_avatarId,
-            @JsonProperty("color") String color,
-            @JsonProperty("blocked") boolean blocked,
-            @JsonProperty("inboxPosition") Integer inboxPosition,
-            @JsonProperty("archived") boolean archived,
-            @JsonProperty("messageExpirationTime") int messageExpirationTime,
-            @JsonProperty("active") boolean _ignored_active
+            final GroupIdV1 groupId,
+            final GroupIdV2 expectedV2Id,
+            final String name,
+            final Set<RecipientId> members,
+            final String color,
+            final int messageExpirationTime,
+            final boolean blocked,
+            final boolean archived
     ) {
-        this.groupId = GroupId.v1(groupId);
-        this.expectedV2Id = GroupId.v2(expectedV2Id);
+        this.groupId = groupId;
+        this.expectedV2Id = expectedV2Id;
         this.name = name;
-        this.members.addAll(members);
+        this.members = members;
         this.color = color;
+        this.messageExpirationTime = messageExpirationTime;
         this.blocked = blocked;
-        this.inboxPosition = inboxPosition;
         this.archived = archived;
-        this.messageExpirationTime = messageExpirationTime;
     }
 
     @Override
-    @JsonIgnore
     public GroupIdV1 getGroupId() {
         return groupId;
     }
 
-    @JsonProperty("groupId")
-    private byte[] getGroupIdJackson() {
-        return groupId.serialize();
-    }
-
-    @JsonIgnore
     public GroupIdV2 getExpectedV2Id() {
         if (expectedV2Id == null) {
             expectedV2Id = GroupUtils.getGroupIdV2(groupId);
@@ -99,11 +60,6 @@ public class GroupInfoV1 extends GroupInfo {
         return expectedV2Id;
     }
 
-    @JsonProperty("expectedV2Id")
-    private byte[] getExpectedV2IdJackson() {
-        return getExpectedV2Id().serialize();
-    }
-
     @Override
     public String getTitle() {
         return name;
@@ -114,8 +70,7 @@ public class GroupInfoV1 extends GroupInfo {
         return null;
     }
 
-    @JsonIgnore
-    public Set<SignalServiceAddress> getMembers() {
+    public Set<RecipientId> getMembers() {
         return members;
     }
 
@@ -134,79 +89,11 @@ public class GroupInfoV1 extends GroupInfo {
         return messageExpirationTime;
     }
 
-    public void addMembers(Collection<SignalServiceAddress> addresses) {
-        for (var address : addresses) {
-            if (this.members.contains(address)) {
-                continue;
-            }
-            removeMember(address);
-            this.members.add(address);
-        }
-    }
-
-    public void removeMember(SignalServiceAddress address) {
-        this.members.removeIf(member -> member.matches(address));
-    }
-
-    private static final class JsonSignalServiceAddress {
-
-        @JsonProperty
-        private UUID uuid;
-
-        @JsonProperty
-        private String number;
-
-        JsonSignalServiceAddress(@JsonProperty("uuid") final UUID uuid, @JsonProperty("number") final String number) {
-            this.uuid = uuid;
-            this.number = number;
-        }
-
-        JsonSignalServiceAddress(SignalServiceAddress address) {
-            this.uuid = address.getUuid().orNull();
-            this.number = address.getNumber().orNull();
-        }
-
-        SignalServiceAddress toSignalServiceAddress() {
-            return new SignalServiceAddress(uuid, number);
-        }
-    }
-
-    private static class MembersSerializer extends JsonSerializer<Set<SignalServiceAddress>> {
-
-        @Override
-        public void serialize(
-                final Set<SignalServiceAddress> value, final JsonGenerator jgen, final SerializerProvider provider
-        ) throws IOException {
-            jgen.writeStartArray(value.size());
-            for (var address : value) {
-                if (address.getUuid().isPresent()) {
-                    jgen.writeObject(new JsonSignalServiceAddress(address));
-                } else {
-                    jgen.writeString(address.getNumber().get());
-                }
-            }
-            jgen.writeEndArray();
-        }
+    public void addMembers(Collection<RecipientId> members) {
+        this.members.addAll(members);
     }
 
-    private static class MembersDeserializer extends JsonDeserializer<Set<SignalServiceAddress>> {
-
-        @Override
-        public Set<SignalServiceAddress> deserialize(
-                JsonParser jsonParser, DeserializationContext deserializationContext
-        ) throws IOException {
-            var addresses = new HashSet<SignalServiceAddress>();
-            JsonNode node = jsonParser.getCodec().readTree(jsonParser);
-            for (var n : node) {
-                if (n.isTextual()) {
-                    addresses.add(new SignalServiceAddress(null, n.textValue()));
-                } else {
-                    var address = jsonProcessor.treeToValue(n, JsonSignalServiceAddress.class);
-                    addresses.add(address.toSignalServiceAddress());
-                }
-            }
-
-            return addresses;
-        }
+    public void removeMember(RecipientId recipientId) {
+        this.members.removeIf(member -> member.equals(recipientId));
     }
 }
index 2092c03aa648a82de6157c4ab2d802e5092f3d92..b37e50a969943c9836fa09cc30e412cf979dd3fb 100644 (file)
@@ -2,6 +2,8 @@ package org.asamk.signal.manager.storage.groups;
 
 import org.asamk.signal.manager.groups.GroupIdV2;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
+import org.asamk.signal.manager.storage.recipients.RecipientResolver;
 import org.signal.storageservice.protos.groups.AccessControl;
 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
 import org.signal.zkgroup.groups.GroupMasterKey;
@@ -18,12 +20,19 @@ public class GroupInfoV2 extends GroupInfo {
 
     private boolean blocked;
     private DecryptedGroup group; // stored as a file with hexadecimal groupId as name
+    private RecipientResolver recipientResolver;
 
     public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) {
         this.groupId = groupId;
         this.masterKey = masterKey;
     }
 
+    public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey, final boolean blocked) {
+        this.groupId = groupId;
+        this.masterKey = masterKey;
+        this.blocked = blocked;
+    }
+
     @Override
     public GroupIdV2 getGroupId() {
         return groupId;
@@ -33,8 +42,9 @@ public class GroupInfoV2 extends GroupInfo {
         return masterKey;
     }
 
-    public void setGroup(final DecryptedGroup group) {
+    public void setGroup(final DecryptedGroup group, final RecipientResolver recipientResolver) {
         this.group = group;
+        this.recipientResolver = recipientResolver;
     }
 
     public DecryptedGroup getGroup() {
@@ -63,35 +73,38 @@ public class GroupInfoV2 extends GroupInfo {
     }
 
     @Override
-    public Set<SignalServiceAddress> getMembers() {
+    public Set<RecipientId> getMembers() {
         if (this.group == null) {
             return Set.of();
         }
         return group.getMembersList()
                 .stream()
                 .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
+                .map(recipientResolver::resolveRecipient)
                 .collect(Collectors.toSet());
     }
 
     @Override
-    public Set<SignalServiceAddress> getPendingMembers() {
+    public Set<RecipientId> getPendingMembers() {
         if (this.group == null) {
             return Set.of();
         }
         return group.getPendingMembersList()
                 .stream()
                 .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
+                .map(recipientResolver::resolveRecipient)
                 .collect(Collectors.toSet());
     }
 
     @Override
-    public Set<SignalServiceAddress> getRequestingMembers() {
+    public Set<RecipientId> getRequestingMembers() {
         if (this.group == null) {
             return Set.of();
         }
         return group.getRequestingMembersList()
                 .stream()
                 .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
+                .map(recipientResolver::resolveRecipient)
                 .collect(Collectors.toSet());
     }
 
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
new file mode 100644 (file)
index 0000000..19fc756
--- /dev/null
@@ -0,0 +1,474 @@
+package org.asamk.signal.manager.storage.groups;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+import org.asamk.signal.manager.groups.GroupId;
+import org.asamk.signal.manager.groups.GroupIdV1;
+import org.asamk.signal.manager.groups.GroupIdV2;
+import org.asamk.signal.manager.groups.GroupUtils;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
+import org.asamk.signal.manager.storage.recipients.RecipientResolver;
+import org.asamk.signal.manager.util.IOUtils;
+import org.signal.storageservice.protos.groups.local.DecryptedGroup;
+import org.signal.zkgroup.InvalidInputException;
+import org.signal.zkgroup.groups.GroupMasterKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.whispersystems.libsignal.util.Hex;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.util.UuidUtil;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class GroupStore {
+
+    private final static Logger logger = LoggerFactory.getLogger(GroupStore.class);
+
+    private final File groupCachePath;
+    private final Map<GroupId, GroupInfo> groups;
+    private final RecipientResolver recipientResolver;
+    private final Saver saver;
+
+    private GroupStore(
+            final File groupCachePath,
+            final Map<GroupId, GroupInfo> groups,
+            final RecipientResolver recipientResolver,
+            final Saver saver
+    ) {
+        this.groupCachePath = groupCachePath;
+        this.groups = groups;
+        this.recipientResolver = recipientResolver;
+        this.saver = saver;
+    }
+
+    public GroupStore(
+            final File groupCachePath, final RecipientResolver recipientResolver, final Saver saver
+    ) {
+        this.groups = new HashMap<>();
+        this.groupCachePath = groupCachePath;
+        this.recipientResolver = recipientResolver;
+        this.saver = saver;
+    }
+
+    public static GroupStore fromStorage(
+            final Storage storage,
+            final File groupCachePath,
+            final RecipientResolver recipientResolver,
+            final Saver saver
+    ) {
+        final var groups = storage.groups.stream().map(g -> {
+            if (g instanceof Storage.GroupV1) {
+                final var g1 = (Storage.GroupV1) g;
+                final var members = g1.members.stream().map(m -> {
+                    if (m.recipientId == null) {
+                        return recipientResolver.resolveRecipient(new SignalServiceAddress(UuidUtil.parseOrNull(m.uuid),
+                                m.number));
+                    }
+
+                    return RecipientId.of(m.recipientId);
+                }).collect(Collectors.toSet());
+
+                return new GroupInfoV1(GroupIdV1.fromBase64(g1.groupId),
+                        g1.expectedV2Id == null ? null : GroupIdV2.fromBase64(g1.expectedV2Id),
+                        g1.name,
+                        members,
+                        g1.color,
+                        g1.messageExpirationTime,
+                        g1.blocked,
+                        g1.archived);
+            }
+
+            final var g2 = (Storage.GroupV2) g;
+            var groupId = GroupIdV2.fromBase64(g2.groupId);
+            GroupMasterKey masterKey;
+            try {
+                masterKey = new GroupMasterKey(Base64.getDecoder().decode(g2.masterKey));
+            } catch (InvalidInputException | IllegalArgumentException e) {
+                throw new AssertionError("Invalid master key for group " + groupId.toBase64());
+            }
+
+            return new GroupInfoV2(groupId, masterKey, g2.blocked);
+        }).collect(Collectors.toMap(GroupInfo::getGroupId, g -> g));
+
+        return new GroupStore(groupCachePath, groups, recipientResolver, saver);
+    }
+
+    public void updateGroup(GroupInfo group) {
+        final Storage storage;
+        synchronized (groups) {
+            groups.put(group.getGroupId(), group);
+            if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
+                try {
+                    IOUtils.createPrivateDirectories(groupCachePath);
+                    try (var stream = new FileOutputStream(getGroupV2File(group.getGroupId()))) {
+                        ((GroupInfoV2) group).getGroup().writeTo(stream);
+                    }
+                    final var groupFileLegacy = getGroupV2FileLegacy(group.getGroupId());
+                    if (groupFileLegacy.exists()) {
+                        groupFileLegacy.delete();
+                    }
+                } catch (IOException e) {
+                    logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
+                }
+            }
+            storage = toStorageLocked();
+        }
+        saver.save(storage);
+    }
+
+    public void deleteGroupV1(GroupIdV1 groupId) {
+        final Storage storage;
+        synchronized (groups) {
+            groups.remove(groupId);
+            storage = toStorageLocked();
+        }
+        saver.save(storage);
+    }
+
+    public GroupInfo getGroup(GroupId groupId) {
+        synchronized (groups) {
+            return getGroupLocked(groupId);
+        }
+    }
+
+    public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
+        synchronized (groups) {
+            var group = getGroupLocked(groupId);
+            if (group instanceof GroupInfoV1) {
+                return (GroupInfoV1) group;
+            }
+
+            if (group == null) {
+                return new GroupInfoV1(groupId);
+            }
+
+            return null;
+        }
+    }
+
+    public List<GroupInfo> getGroups() {
+        synchronized (groups) {
+            final var groups = this.groups.values();
+            for (var group : groups) {
+                loadDecryptedGroupLocked(group);
+            }
+            return new ArrayList<>(groups);
+        }
+    }
+
+    public void mergeRecipients(final RecipientId recipientId, final RecipientId toBeMergedRecipientId) {
+        synchronized (groups) {
+            var modified = false;
+            for (var group : this.groups.values()) {
+                if (group instanceof GroupInfoV1) {
+                    var groupV1 = (GroupInfoV1) group;
+                    if (groupV1.isMember(toBeMergedRecipientId)) {
+                        groupV1.removeMember(toBeMergedRecipientId);
+                        groupV1.addMembers(List.of(recipientId));
+                        modified = true;
+                    }
+                }
+            }
+            if (modified) {
+                saver.save(toStorageLocked());
+            }
+        }
+    }
+
+    private GroupInfo getGroupLocked(final GroupId groupId) {
+        var group = groups.get(groupId);
+        if (group == null) {
+            if (groupId instanceof GroupIdV1) {
+                group = getGroupByV1IdLocked((GroupIdV1) groupId);
+            } else if (groupId instanceof GroupIdV2) {
+                group = getGroupV1ByV2IdLocked((GroupIdV2) groupId);
+            }
+        }
+        loadDecryptedGroupLocked(group);
+        return group;
+    }
+
+    private GroupInfo getGroupByV1IdLocked(final GroupIdV1 groupId) {
+        return groups.get(GroupUtils.getGroupIdV2(groupId));
+    }
+
+    private GroupInfoV1 getGroupV1ByV2IdLocked(GroupIdV2 groupIdV2) {
+        for (var g : groups.values()) {
+            if (g instanceof GroupInfoV1) {
+                final var gv1 = (GroupInfoV1) g;
+                if (groupIdV2.equals(gv1.getExpectedV2Id())) {
+                    return gv1;
+                }
+            }
+        }
+        return null;
+    }
+
+    private void loadDecryptedGroupLocked(final GroupInfo group) {
+        if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
+            var groupFile = getGroupV2File(group.getGroupId());
+            if (!groupFile.exists()) {
+                groupFile = getGroupV2FileLegacy(group.getGroupId());
+            }
+            if (!groupFile.exists()) {
+                return;
+            }
+            try (var stream = new FileInputStream(groupFile)) {
+                ((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream), recipientResolver);
+            } catch (IOException ignored) {
+            }
+        }
+    }
+
+    private File getGroupV2FileLegacy(final GroupId groupId) {
+        return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
+    }
+
+    private File getGroupV2File(final GroupId groupId) {
+        return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
+    }
+
+    private Storage toStorageLocked() {
+        return new Storage(groups.values().stream().map(g -> {
+            if (g instanceof GroupInfoV1) {
+                final var g1 = (GroupInfoV1) g;
+                return new Storage.GroupV1(g1.getGroupId().toBase64(),
+                        g1.getExpectedV2Id().toBase64(),
+                        g1.name,
+                        g1.color,
+                        g1.messageExpirationTime,
+                        g1.blocked,
+                        g1.archived,
+                        g1.members.stream()
+                                .map(m -> new Storage.GroupV1.Member(m.getId(), null, null))
+                                .collect(Collectors.toList()));
+            }
+
+            final var g2 = (GroupInfoV2) g;
+            return new Storage.GroupV2(g2.getGroupId().toBase64(),
+                    Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()),
+                    g2.isBlocked());
+        }).collect(Collectors.toList()));
+    }
+
+    public static class Storage {
+
+        //        @JsonSerialize(using = GroupsSerializer.class)
+        @JsonDeserialize(using = GroupsDeserializer.class)
+        public List<Storage.Group> groups;
+
+        // For deserialization
+        public Storage() {
+        }
+
+        public Storage(final List<Storage.Group> groups) {
+            this.groups = groups;
+        }
+
+        private abstract static class Group {
+
+        }
+
+        private static class GroupV1 extends Group {
+
+            public String groupId;
+            public String expectedV2Id;
+            public String name;
+            public String color;
+            public int messageExpirationTime;
+            public boolean blocked;
+            public boolean archived;
+
+            @JsonDeserialize(using = MembersDeserializer.class)
+            @JsonSerialize(using = MembersSerializer.class)
+            public List<Member> members;
+
+            // For deserialization
+            public GroupV1() {
+            }
+
+            public GroupV1(
+                    final String groupId,
+                    final String expectedV2Id,
+                    final String name,
+                    final String color,
+                    final int messageExpirationTime,
+                    final boolean blocked,
+                    final boolean archived,
+                    final List<Member> members
+            ) {
+                this.groupId = groupId;
+                this.expectedV2Id = expectedV2Id;
+                this.name = name;
+                this.color = color;
+                this.messageExpirationTime = messageExpirationTime;
+                this.blocked = blocked;
+                this.archived = archived;
+                this.members = members;
+            }
+
+            private static final class Member {
+
+                public Long recipientId;
+
+                public String uuid;
+
+                public String number;
+
+                Member(Long recipientId, final String uuid, final String number) {
+                    this.recipientId = recipientId;
+                    this.uuid = uuid;
+                    this.number = number;
+                }
+            }
+
+            private static final class JsonSignalServiceAddress {
+
+                public String uuid;
+
+                public String number;
+
+                // For deserialization
+                public JsonSignalServiceAddress() {
+                }
+
+                JsonSignalServiceAddress(final String uuid, final String number) {
+                    this.uuid = uuid;
+                    this.number = number;
+                }
+            }
+
+            private static class MembersSerializer extends JsonSerializer<List<Member>> {
+
+                @Override
+                public void serialize(
+                        final List<Member> value, final JsonGenerator jgen, final SerializerProvider provider
+                ) throws IOException {
+                    jgen.writeStartArray(value.size());
+                    for (var address : value) {
+                        if (address.recipientId != null) {
+                            jgen.writeNumber(address.recipientId);
+                        } else if (address.uuid != null) {
+                            jgen.writeObject(new JsonSignalServiceAddress(address.uuid, address.number));
+                        } else {
+                            jgen.writeString(address.number);
+                        }
+                    }
+                    jgen.writeEndArray();
+                }
+            }
+
+            private static class MembersDeserializer extends JsonDeserializer<List<Member>> {
+
+                @Override
+                public List<Member> deserialize(
+                        JsonParser jsonParser, DeserializationContext deserializationContext
+                ) throws IOException {
+                    var addresses = new ArrayList<Member>();
+                    JsonNode node = jsonParser.getCodec().readTree(jsonParser);
+                    for (var n : node) {
+                        if (n.isTextual()) {
+                            addresses.add(new Member(null, null, n.textValue()));
+                        } else if (n.isNumber()) {
+                            addresses.add(new Member(n.numberValue().longValue(), null, null));
+                        } else {
+                            var address = jsonParser.getCodec().treeToValue(n, JsonSignalServiceAddress.class);
+                            addresses.add(new Member(null, address.uuid, address.number));
+                        }
+                    }
+
+                    return addresses;
+                }
+            }
+        }
+
+        private static class GroupV2 extends Group {
+
+            public String groupId;
+            public String masterKey;
+            public boolean blocked;
+
+            // For deserialization
+            private GroupV2() {
+            }
+
+            public GroupV2(final String groupId, final String masterKey, final boolean blocked) {
+                this.groupId = groupId;
+                this.masterKey = masterKey;
+                this.blocked = blocked;
+            }
+        }
+
+    }
+
+    //    private static class GroupsSerializer extends JsonSerializer<List<Storage.Group>> {
+//
+//        @Override
+//        public void serialize(
+//                final List<Storage.Group> groups, final JsonGenerator jgen, final SerializerProvider provider
+//        ) throws IOException {
+//            jgen.writeStartArray(groups.size());
+//            for (var group : groups) {
+//                if (group instanceof GroupInfoV1) {
+//                    jgen.writeObject(group);
+//                } else if (group instanceof GroupInfoV2) {
+//                    final var groupV2 = (GroupInfoV2) group;
+//                    jgen.writeStartObject();
+//                    jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
+//                    jgen.writeStringField("masterKey",
+//                            Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize()));
+//                    jgen.writeBooleanField("blocked", groupV2.isBlocked());
+//                    jgen.writeEndObject();
+//                } else {
+//                    throw new AssertionError("Unknown group version");
+//                }
+//            }
+//            jgen.writeEndArray();
+//        }
+//    }
+//
+    private static class GroupsDeserializer extends JsonDeserializer<List<Storage.Group>> {
+
+        @Override
+        public List<Storage.Group> deserialize(
+                JsonParser jsonParser, DeserializationContext deserializationContext
+        ) throws IOException {
+            var groups = new ArrayList<Storage.Group>();
+            JsonNode node = jsonParser.getCodec().readTree(jsonParser);
+            for (var n : node) {
+                Storage.Group g;
+                if (n.hasNonNull("masterKey")) {
+                    // a v2 group
+                    g = jsonParser.getCodec().treeToValue(n, Storage.GroupV2.class);
+                } else {
+                    g = jsonParser.getCodec().treeToValue(n, Storage.GroupV1.class);
+                }
+                groups.add(g);
+            }
+
+            return groups;
+        }
+    }
+
+    public interface Saver {
+
+        void save(Storage storage);
+    }
+}
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java
deleted file mode 100644 (file)
index 4bd19ea..0000000
+++ /dev/null
@@ -1,207 +0,0 @@
-package org.asamk.signal.manager.storage.groups;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.JsonSerializer;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializerProvider;
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
-import com.fasterxml.jackson.databind.annotation.JsonSerialize;
-
-import org.asamk.signal.manager.groups.GroupId;
-import org.asamk.signal.manager.groups.GroupIdV1;
-import org.asamk.signal.manager.groups.GroupIdV2;
-import org.asamk.signal.manager.groups.GroupUtils;
-import org.asamk.signal.manager.util.IOUtils;
-import org.signal.storageservice.protos.groups.local.DecryptedGroup;
-import org.signal.zkgroup.InvalidInputException;
-import org.signal.zkgroup.groups.GroupMasterKey;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.whispersystems.libsignal.util.Hex;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class JsonGroupStore {
-
-    private final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class);
-
-    private static final ObjectMapper jsonProcessor = new ObjectMapper();
-    @JsonIgnore
-    public File groupCachePath;
-
-    @JsonProperty("groups")
-    @JsonSerialize(using = GroupsSerializer.class)
-    @JsonDeserialize(using = GroupsDeserializer.class)
-    private final Map<GroupId, GroupInfo> groups = new HashMap<>();
-
-    private JsonGroupStore() {
-    }
-
-    public JsonGroupStore(final File groupCachePath) {
-        this.groupCachePath = groupCachePath;
-    }
-
-    public void updateGroup(GroupInfo group) {
-        groups.put(group.getGroupId(), group);
-        if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
-            try {
-                IOUtils.createPrivateDirectories(groupCachePath);
-                try (var stream = new FileOutputStream(getGroupFile(group.getGroupId()))) {
-                    ((GroupInfoV2) group).getGroup().writeTo(stream);
-                }
-                final var groupFileLegacy = getGroupFileLegacy(group.getGroupId());
-                if (groupFileLegacy.exists()) {
-                    groupFileLegacy.delete();
-                }
-            } catch (IOException e) {
-                logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
-            }
-        }
-    }
-
-    public void deleteGroup(GroupId groupId) {
-        groups.remove(groupId);
-    }
-
-    public GroupInfo getGroup(GroupId groupId) {
-        var group = groups.get(groupId);
-        if (group == null) {
-            if (groupId instanceof GroupIdV1) {
-                group = groups.get(GroupUtils.getGroupIdV2((GroupIdV1) groupId));
-            } else if (groupId instanceof GroupIdV2) {
-                group = getGroupV1ByV2Id((GroupIdV2) groupId);
-            }
-        }
-        loadDecryptedGroup(group);
-        return group;
-    }
-
-    private GroupInfoV1 getGroupV1ByV2Id(GroupIdV2 groupIdV2) {
-        for (var g : groups.values()) {
-            if (g instanceof GroupInfoV1) {
-                final var gv1 = (GroupInfoV1) g;
-                if (groupIdV2.equals(gv1.getExpectedV2Id())) {
-                    return gv1;
-                }
-            }
-        }
-        return null;
-    }
-
-    private void loadDecryptedGroup(final GroupInfo group) {
-        if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
-            var groupFile = getGroupFile(group.getGroupId());
-            if (!groupFile.exists()) {
-                groupFile = getGroupFileLegacy(group.getGroupId());
-            }
-            if (!groupFile.exists()) {
-                return;
-            }
-            try (var stream = new FileInputStream(groupFile)) {
-                ((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream));
-            } catch (IOException ignored) {
-            }
-        }
-    }
-
-    private File getGroupFileLegacy(final GroupId groupId) {
-        return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
-    }
-
-    private File getGroupFile(final GroupId groupId) {
-        return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
-    }
-
-    public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
-        var group = getGroup(groupId);
-        if (group instanceof GroupInfoV1) {
-            return (GroupInfoV1) group;
-        }
-
-        if (group == null) {
-            return new GroupInfoV1(groupId);
-        }
-
-        return null;
-    }
-
-    @JsonIgnore
-    public List<GroupInfo> getGroups() {
-        final var groups = this.groups.values();
-        for (var group : groups) {
-            loadDecryptedGroup(group);
-        }
-        return new ArrayList<>(groups);
-    }
-
-    private static class GroupsSerializer extends JsonSerializer<Map<String, GroupInfo>> {
-
-        @Override
-        public void serialize(
-                final Map<String, GroupInfo> value, final JsonGenerator jgen, final SerializerProvider provider
-        ) throws IOException {
-            final var groups = value.values();
-            jgen.writeStartArray(groups.size());
-            for (var group : groups) {
-                if (group instanceof GroupInfoV1) {
-                    jgen.writeObject(group);
-                } else if (group instanceof GroupInfoV2) {
-                    final var groupV2 = (GroupInfoV2) group;
-                    jgen.writeStartObject();
-                    jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
-                    jgen.writeStringField("masterKey",
-                            Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize()));
-                    jgen.writeBooleanField("blocked", groupV2.isBlocked());
-                    jgen.writeEndObject();
-                } else {
-                    throw new AssertionError("Unknown group version");
-                }
-            }
-            jgen.writeEndArray();
-        }
-    }
-
-    private static class GroupsDeserializer extends JsonDeserializer<Map<GroupId, GroupInfo>> {
-
-        @Override
-        public Map<GroupId, GroupInfo> deserialize(
-                JsonParser jsonParser, DeserializationContext deserializationContext
-        ) throws IOException {
-            var groups = new HashMap<GroupId, GroupInfo>();
-            JsonNode node = jsonParser.getCodec().readTree(jsonParser);
-            for (var n : node) {
-                GroupInfo g;
-                if (n.hasNonNull("masterKey")) {
-                    // a v2 group
-                    var groupId = GroupIdV2.fromBase64(n.get("groupId").asText());
-                    try {
-                        var masterKey = new GroupMasterKey(Base64.getDecoder().decode(n.get("masterKey").asText()));
-                        g = new GroupInfoV2(groupId, masterKey);
-                    } catch (InvalidInputException | IllegalArgumentException e) {
-                        throw new AssertionError("Invalid master key for group " + groupId.toBase64());
-                    }
-                    g.setBlocked(n.get("blocked").asBoolean(false));
-                } else {
-                    g = jsonProcessor.treeToValue(n, GroupInfoV1.class);
-                }
-                groups.put(g.getGroupId(), g);
-            }
-
-            return groups;
-        }
-    }
-}
index bcd88ab415265b849a02408e76e201599e097d72..1fc38c7bfa553db0f9ad18c0b6ca512690b3c1f4 100644 (file)
@@ -46,7 +46,7 @@ public class JoinGroupCommand implements LocalCommand {
 
             final var results = m.joinGroup(linkUrl);
             var newGroupId = results.first();
-            if (!m.getGroup(newGroupId).isMember(m.getSelfAddress())) {
+            if (!m.getGroup(newGroupId).isMember(m.getSelfRecipientId())) {
                 writer.println("Requested to join group \"{}\"", newGroupId.toBase64());
             } else {
                 writer.println("Joined group \"{}\"", newGroupId.toBase64());
index a547cc15588bf9f54d9ead385419ddbde730ecec..a3a5560838531e0028fe34664fc6730673c49291 100644 (file)
@@ -11,6 +11,7 @@ import org.asamk.signal.PlainTextWriterImpl;
 import org.asamk.signal.commands.exceptions.CommandException;
 import org.asamk.signal.manager.Manager;
 import org.asamk.signal.manager.storage.groups.GroupInfo;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@@ -23,7 +24,7 @@ public class ListGroupsCommand implements LocalCommand {
 
     private final static Logger logger = LoggerFactory.getLogger(ListGroupsCommand.class);
 
-    private static Set<String> resolveMembers(Manager m, Set<SignalServiceAddress> addresses) {
+    private static Set<String> resolveMembers(Manager m, Set<RecipientId> addresses) {
         return addresses.stream()
                 .map(m::resolveSignalServiceAddress)
                 .map(SignalServiceAddress::getLegacyIdentifier)
@@ -40,7 +41,7 @@ public class ListGroupsCommand implements LocalCommand {
                     "Id: {} Name: {}  Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Link: {}",
                     group.getGroupId().toBase64(),
                     group.getTitle(),
-                    group.isMember(m.getSelfAddress()),
+                    group.isMember(m.getSelfRecipientId()),
                     group.isBlocked(),
                     resolveMembers(m, group.getMembers()),
                     resolveMembers(m, group.getPendingMembers()),
@@ -50,7 +51,7 @@ public class ListGroupsCommand implements LocalCommand {
             writer.println("Id: {} Name: {}  Active: {} Blocked: {}",
                     group.getGroupId().toBase64(),
                     group.getTitle(),
-                    group.isMember(m.getSelfAddress()),
+                    group.isMember(m.getSelfRecipientId()),
                     group.isBlocked());
         }
     }
@@ -80,7 +81,7 @@ public class ListGroupsCommand implements LocalCommand {
 
                 jsonGroups.add(new JsonGroup(group.getGroupId().toBase64(),
                         group.getTitle(),
-                        group.isMember(m.getSelfAddress()),
+                        group.isMember(m.getSelfRecipientId()),
                         group.isBlocked(),
                         resolveMembers(m, group.getMembers()),
                         resolveMembers(m, group.getPendingMembers()),
index e58f78299cf7b1c9b6e9218183390e093a6cd68b..ae19c97883727adfee3a75a49e4f4296fb276655 100644 (file)
@@ -472,7 +472,7 @@ public class DbusSignalImpl implements Signal {
         if (group == null) {
             return false;
         } else {
-            return group.isMember(m.getSelfAddress());
+            return group.isMember(m.getSelfRecipientId());
         }
     }
 }