X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/d520023fc76a522650b7561f2a4fc7a95fb5a04d..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 94935441..4f9d8628 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -13,12 +13,15 @@ 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; 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; @@ -33,11 +36,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 +72,14 @@ public class SignalAccount implements Closeable { private JsonGroupStore groupStore; 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); @@ -83,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(); @@ -92,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()) { @@ -105,15 +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()) { @@ -130,9 +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; @@ -143,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; @@ -151,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); @@ -194,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"); @@ -230,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()) { @@ -244,9 +278,26 @@ 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 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()) { @@ -259,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); } } @@ -290,13 +341,19 @@ public class SignalAccount implements Closeable { .putPOJO("groupStore", groupStore) .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())); @@ -346,6 +403,14 @@ public class SignalAccount implements Closeable { return recipientStore; } + public ProfileStore getProfileStore() { + return profileStore; + } + + public StickerStore getStickerStore() { + return stickerStore; + } + public String getUsername() { return username; } @@ -429,7 +494,10 @@ public class SignalAccount implements Closeable { @Override public void close() throws IOException { synchronized (fileChannel) { - lock.close(); + try { + lock.close(); + } catch (ClosedChannelException ignored) { + } fileChannel.close(); } }