X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/b94c1e50e62946a4d774a4c53ce70858145a4422..591c0fe8a3744608575a6dcb1f6f4f9f818948d2:/src/main/java/org/asamk/signal/storage/SignalAccount.java diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index d9b37825..4f9d8628 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -13,6 +13,7 @@ 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; @@ -20,6 +21,7 @@ import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; import org.asamk.signal.storage.protocol.RecipientStore; import org.asamk.signal.storage.protocol.SessionInfo; import org.asamk.signal.storage.protocol.SignalServiceAddressResolver; +import org.asamk.signal.storage.stickers.StickerStore; import org.asamk.signal.storage.threads.LegacyJsonThreadStore; import org.asamk.signal.storage.threads.ThreadInfo; import org.asamk.signal.util.IOUtils; @@ -34,6 +36,8 @@ 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; @@ -69,13 +73,13 @@ public class SignalAccount implements Closeable { private JsonContactsStore contactStore; private RecipientStore recipientStore; private ProfileStore profileStore; + private StickerStore stickerStore; 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); @@ -86,7 +90,7 @@ public class SignalAccount implements Closeable { final Pair 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(); @@ -95,7 +99,9 @@ public class SignalAccount implements Closeable { } } - public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey) throws IOException { + public static SignalAccount create( + String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey + ) throws IOException { IOUtils.createPrivateDirectories(dataPath); String fileName = getFileName(dataPath, username); if (!new File(fileName).exists()) { @@ -108,16 +114,27 @@ 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.stickerStore = new StickerStore(); account.registered = false; return account; } - public static SignalAccount createLinkedAccount(String dataPath, String username, UUID uuid, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, ProfileKey profileKey) throws IOException { + public static SignalAccount createLinkedAccount( + String dataPath, + String username, + UUID uuid, + String password, + int deviceId, + IdentityKeyPair identityKey, + int registrationId, + String signalingKey, + ProfileKey profileKey + ) throws IOException { IOUtils.createPrivateDirectories(dataPath); String fileName = getFileName(dataPath, username); if (!new File(fileName).exists()) { @@ -134,10 +151,11 @@ 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.stickerStore = new StickerStore(); account.registered = true; account.isMultiDevice = true; @@ -148,6 +166,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; @@ -156,7 +178,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); @@ -199,18 +221,22 @@ public class SignalAccount implements Closeable { try { profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText())); } catch (InvalidInputException e) { - throw new IOException("Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes", e); + throw new IOException( + "Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes", + e); } } - signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); + signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), + JsonSignalProtocolStore.class); registered = Util.getNotNullNode(rootNode, "registered").asBoolean(); 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"); @@ -235,9 +261,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()) { @@ -257,9 +286,18 @@ public class SignalAccount implements Closeable { profileStore = new ProfileStore(); } + JsonNode stickerStoreNode = rootNode.get("stickerStore"); + if (stickerStoreNode != null) { + stickerStore = jsonProcessor.convertValue(stickerStoreNode, StickerStore.class); + } + if (stickerStore == null) { + stickerStore = new StickerStore(); + } + JsonNode threadStoreNode = rootNode.get("threadStore"); if (threadStoreNode != null) { - LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class); + LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, + LegacyJsonThreadStore.class); // Migrate thread info to group and contact store for (ThreadInfo thread : threadStore.getThreads()) { if (thread.id == null || thread.id.isEmpty()) { @@ -272,8 +310,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); } } @@ -304,13 +342,18 @@ public class SignalAccount implements Closeable { .putPOJO("contactStore", contactStore) .putPOJO("recipientStore", recipientStore) .putPOJO("profileStore", profileStore) - ; + .putPOJO("stickerStore", stickerStore); 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())); @@ -364,6 +407,10 @@ public class SignalAccount implements Closeable { return profileStore; } + public StickerStore getStickerStore() { + return stickerStore; + } + public String getUsername() { return username; }