]> nmode's Git Repositories - signal-cli/blobdiff - src/main/java/org/asamk/signal/storage/SignalAccount.java
Implement support for sending/receiving Group V2 messages
[signal-cli] / src / main / java / org / asamk / signal / storage / SignalAccount.java
index 9493544162bff1df6678ef4d842509a5c541c638..d3c2506dc578f07e488fa6c53b9e2a8a340a763f 100644 (file)
@@ -13,7 +13,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.asamk.signal.storage.contacts.ContactInfo;
 import org.asamk.signal.storage.contacts.JsonContactsStore;
 import org.asamk.signal.storage.groups.GroupInfo;
+import org.asamk.signal.storage.groups.GroupInfoV1;
 import org.asamk.signal.storage.groups.JsonGroupStore;
+import org.asamk.signal.storage.profiles.ProfileStore;
 import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
 import org.asamk.signal.storage.protocol.JsonSignalProtocolStore;
 import org.asamk.signal.storage.protocol.RecipientStore;
@@ -33,11 +35,14 @@ import org.whispersystems.libsignal.util.Pair;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 import org.whispersystems.util.Base64;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
 import java.io.RandomAccessFile;
 import java.nio.channels.Channels;
+import java.nio.channels.ClosedChannelException;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
 import java.util.Collection;
@@ -66,13 +71,13 @@ public class SignalAccount implements Closeable {
     private JsonGroupStore groupStore;
     private JsonContactsStore contactStore;
     private RecipientStore recipientStore;
+    private ProfileStore profileStore;
 
     private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
         this.fileChannel = fileChannel;
         this.lock = lock;
         jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
         jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
-        jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
         jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
         jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
         jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
@@ -83,7 +88,7 @@ public class SignalAccount implements Closeable {
         final Pair<FileChannel, FileLock> pair = openFileChannel(fileName);
         try {
             SignalAccount account = new SignalAccount(pair.first(), pair.second());
-            account.load();
+            account.load(dataPath);
             return account;
         } catch (Throwable e) {
             pair.second().close();
@@ -105,9 +110,10 @@ public class SignalAccount implements Closeable {
         account.username = username;
         account.profileKey = profileKey;
         account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
-        account.groupStore = new JsonGroupStore();
+        account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
         account.contactStore = new JsonContactsStore();
         account.recipientStore = new RecipientStore();
+        account.profileStore = new ProfileStore();
         account.registered = false;
 
         return account;
@@ -130,9 +136,10 @@ public class SignalAccount implements Closeable {
         account.deviceId = deviceId;
         account.signalingKey = signalingKey;
         account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
-        account.groupStore = new JsonGroupStore();
+        account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
         account.contactStore = new JsonContactsStore();
         account.recipientStore = new RecipientStore();
+        account.profileStore = new ProfileStore();
         account.registered = true;
         account.isMultiDevice = true;
 
@@ -143,6 +150,10 @@ public class SignalAccount implements Closeable {
         return dataPath + "/" + username;
     }
 
+    private static File getGroupCachePath(String dataPath, String username) {
+        return new File(new File(dataPath, username + ".d"), "group-cache");
+    }
+
     public static boolean userExists(String dataPath, String username) {
         if (username == null) {
             return false;
@@ -151,7 +162,7 @@ public class SignalAccount implements Closeable {
         return !(!f.exists() || f.isDirectory());
     }
 
-    private void load() throws IOException {
+    private void load(String dataPath) throws IOException {
         JsonNode rootNode;
         synchronized (fileChannel) {
             fileChannel.position(0);
@@ -203,9 +214,10 @@ public class SignalAccount implements Closeable {
         JsonNode groupStoreNode = rootNode.get("groupStore");
         if (groupStoreNode != null) {
             groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
+            groupStore.groupCachePath = getGroupCachePath(dataPath, username);
         }
         if (groupStore == null) {
-            groupStore = new JsonGroupStore();
+            groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
         }
 
         JsonNode contactStoreNode = rootNode.get("contactStore");
@@ -230,9 +242,12 @@ public class SignalAccount implements Closeable {
             }
 
             for (GroupInfo group : groupStore.getGroups()) {
-                group.members = group.members.stream()
-                        .map(m -> recipientStore.resolveServiceAddress(m))
-                        .collect(Collectors.toSet());
+                if (group instanceof GroupInfoV1) {
+                    GroupInfoV1 groupInfoV1 = (GroupInfoV1) group;
+                    groupInfoV1.members = groupInfoV1.members.stream()
+                            .map(m -> recipientStore.resolveServiceAddress(m))
+                            .collect(Collectors.toSet());
+                }
             }
 
             for (SessionInfo session : signalProtocolStore.getSessions()) {
@@ -244,6 +259,14 @@ public class SignalAccount implements Closeable {
             }
         }
 
+        JsonNode profileStoreNode = rootNode.get("profileStore");
+        if (profileStoreNode != null) {
+            profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class);
+        }
+        if (profileStore == null) {
+            profileStore = new ProfileStore();
+        }
+
         JsonNode threadStoreNode = rootNode.get("threadStore");
         if (threadStoreNode != null) {
             LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class);
@@ -259,8 +282,8 @@ public class SignalAccount implements Closeable {
                         contactStore.updateContact(contactInfo);
                     } else {
                         GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id));
-                        if (groupInfo != null) {
-                            groupInfo.messageExpirationTime = thread.messageExpirationTime;
+                        if (groupInfo instanceof GroupInfoV1) {
+                            ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
                             groupStore.updateGroup(groupInfo);
                         }
                     }
@@ -290,13 +313,19 @@ public class SignalAccount implements Closeable {
                 .putPOJO("groupStore", groupStore)
                 .putPOJO("contactStore", contactStore)
                 .putPOJO("recipientStore", recipientStore)
+                .putPOJO("profileStore", profileStore)
         ;
         try {
-            synchronized (fileChannel) {
-                fileChannel.position(0);
-                jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode);
-                fileChannel.truncate(fileChannel.position());
-                fileChannel.force(false);
+            try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
+                // Write to memory first to prevent corrupting the file in case of serialization errors
+                jsonProcessor.writeValue(output, rootNode);
+                ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
+                synchronized (fileChannel) {
+                    fileChannel.position(0);
+                    input.transferTo(Channels.newOutputStream(fileChannel));
+                    fileChannel.truncate(fileChannel.position());
+                    fileChannel.force(false);
+                }
             }
         } catch (Exception e) {
             System.err.println(String.format("Error saving file: %s", e.getMessage()));
@@ -346,6 +375,10 @@ public class SignalAccount implements Closeable {
         return recipientStore;
     }
 
+    public ProfileStore getProfileStore() {
+        return profileStore;
+    }
+
     public String getUsername() {
         return username;
     }
@@ -429,7 +462,10 @@ public class SignalAccount implements Closeable {
     @Override
     public void close() throws IOException {
         synchronized (fileChannel) {
-            lock.close();
+            try {
+                lock.close();
+            } catch (ClosedChannelException ignored) {
+            }
             fileChannel.close();
         }
     }