]> nmode's Git Repositories - signal-cli/commitdiff
Move configuration store to db
authorAsamK <asamk@gmx.de>
Wed, 4 Oct 2023 17:44:45 +0000 (19:44 +0200)
committerAsamK <asamk@gmx.de>
Wed, 4 Oct 2023 18:19:07 +0000 (20:19 +0200)
graalvm-config-dir/reflect-config.json
lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java
lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
lib/src/main/java/org/asamk/signal/manager/storage/configuration/ConfigurationStore.java
lib/src/main/java/org/asamk/signal/manager/storage/configuration/LegacyConfigurationStore.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueEntry.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueStore.java [new file with mode: 0644]

index 3125a44e8da239cc86e4075ac11bf457e8401885..98af166efb13d2228f2fd34ec552b68ceb0c76e2 100644 (file)
   "queryAllDeclaredConstructors":true,
   "methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","org.asamk.signal.manager.api.PhoneNumberSharingMode"] }, {"name":"linkPreviews","parameterTypes":[] }, {"name":"phoneNumberSharingMode","parameterTypes":[] }, {"name":"phoneNumberUnlisted","parameterTypes":[] }, {"name":"readReceipts","parameterTypes":[] }, {"name":"typingIndicators","parameterTypes":[] }, {"name":"unidentifiedDeliveryIndicators","parameterTypes":[] }]
 },
+{
+  "name":"org.asamk.signal.manager.storage.configuration.LegacyConfigurationStore$Storage",
+  "allDeclaredFields":true,
+  "queryAllDeclaredMethods":true,
+  "queryAllDeclaredConstructors":true,
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","org.asamk.signal.manager.api.PhoneNumberSharingMode"] }]
+},
 {
   "name":"org.asamk.signal.manager.storage.contacts.LegacyContactInfo",
   "allDeclaredFields":true,
index fb34b4409ea560aaf4baf5d0b80107978f650dc2..d9839a1876b79fb6d5a4f8d436896d97349a2d8c 100644 (file)
@@ -30,7 +30,7 @@ import java.util.UUID;
 public class AccountDatabase extends Database {
 
     private final static Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
-    private static final long DATABASE_VERSION = 16;
+    private static final long DATABASE_VERSION = 17;
 
     private AccountDatabase(final HikariDataSource dataSource) {
         super(logger, DATABASE_VERSION, dataSource);
@@ -503,5 +503,17 @@ public class AccountDatabase extends Database {
                                         """);
             }
         }
+        if (oldVersion < 17) {
+            logger.debug("Updating database: Adding key_value table");
+            try (final var statement = connection.createStatement()) {
+                statement.executeUpdate("""
+                                        CREATE TABLE key_value (
+                                                _id INTEGER PRIMARY KEY,
+                                                key TEXT UNIQUE NOT NULL,
+                                                value ANY
+                                        ) STRICT;
+                                        """);
+            }
+        }
     }
 }
index aef1d1c91c379a99bc885f66cbb4efb8b40498ff..16017d5e0f21282e8f57cb333583cee89f82b412 100644 (file)
@@ -12,6 +12,7 @@ import org.asamk.signal.manager.api.ServiceEnvironment;
 import org.asamk.signal.manager.api.TrustLevel;
 import org.asamk.signal.manager.helper.RecipientAddressResolver;
 import org.asamk.signal.manager.storage.configuration.ConfigurationStore;
+import org.asamk.signal.manager.storage.configuration.LegacyConfigurationStore;
 import org.asamk.signal.manager.storage.contacts.ContactsStore;
 import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
@@ -20,6 +21,7 @@ import org.asamk.signal.manager.storage.groups.LegacyGroupStore;
 import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
 import org.asamk.signal.manager.storage.identities.LegacyIdentityKeyStore;
 import org.asamk.signal.manager.storage.identities.SignalIdentityKeyStore;
+import org.asamk.signal.manager.storage.keyValue.KeyValueStore;
 import org.asamk.signal.manager.storage.messageCache.MessageCache;
 import org.asamk.signal.manager.storage.prekeys.KyberPreKeyStore;
 import org.asamk.signal.manager.storage.prekeys.LegacyPreKeyStore;
@@ -151,7 +153,7 @@ public class SignalAccount implements Closeable {
     private RecipientStore recipientStore;
     private StickerStore stickerStore;
     private ConfigurationStore configurationStore;
-    private ConfigurationStore.Storage configurationStoreStorage;
+    private KeyValueStore keyValueStore;
 
     private MessageCache messageCache;
     private MessageSendLogStore messageSendLogStore;
@@ -216,7 +218,6 @@ public class SignalAccount implements Closeable {
         signalAccount.aciAccountData.setLocalRegistrationId(registrationId);
         signalAccount.pniAccountData.setLocalRegistrationId(pniRegistrationId);
         signalAccount.settings = settings;
-        signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore);
 
         signalAccount.registered = false;
 
@@ -266,8 +267,6 @@ public class SignalAccount implements Closeable {
                 pniIdentityKey,
                 profileKey);
 
-        signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore);
-
         signalAccount.getRecipientTrustedResolver()
                 .resolveSelfRecipientTrusted(signalAccount.getSelfRecipientAddress());
         signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION;
@@ -774,12 +773,10 @@ public class SignalAccount implements Closeable {
         }
 
         if (rootNode.hasNonNull("configurationStore")) {
-            configurationStoreStorage = jsonProcessor.convertValue(rootNode.get("configurationStore"),
-                    ConfigurationStore.Storage.class);
-            configurationStore = ConfigurationStore.fromStorage(configurationStoreStorage,
-                    this::saveConfigurationStore);
-        } else {
-            configurationStore = new ConfigurationStore(this::saveConfigurationStore);
+            final var configurationStoreStorage = jsonProcessor.convertValue(rootNode.get("configurationStore"),
+                    LegacyConfigurationStore.Storage.class);
+            LegacyConfigurationStore.migrate(configurationStoreStorage, getConfigurationStore());
+            migratedLegacyConfig = true;
         }
 
         migratedLegacyConfig = loadLegacyThreadStore(rootNode) || migratedLegacyConfig;
@@ -965,11 +962,6 @@ public class SignalAccount implements Closeable {
         return false;
     }
 
-    private void saveConfigurationStore(ConfigurationStore.Storage storage) {
-        this.configurationStoreStorage = storage;
-        save();
-    }
-
     private void save() {
         synchronized (fileChannel) {
             var rootNode = jsonProcessor.createObjectNode();
@@ -1028,8 +1020,7 @@ public class SignalAccount implements Closeable {
                             pniAccountData.getPreKeyMetadata().activeLastResortKyberPreKeyId)
                     .put("profileKey",
                             profileKey == null ? null : Base64.getEncoder().encodeToString(profileKey.serialize()))
-                    .put("registered", registered)
-                    .putPOJO("configurationStore", configurationStoreStorage);
+                    .put("registered", registered);
             try {
                 try (var output = new ByteArrayOutputStream()) {
                     // Write to memory first to prevent corrupting the file in case of serialization errors
@@ -1295,8 +1286,13 @@ public class SignalAccount implements Closeable {
         return getOrCreate(() -> senderKeyStore, () -> senderKeyStore = new SenderKeyStore(getAccountDatabase()));
     }
 
+    private KeyValueStore getKeyValueStore() {
+        return getOrCreate(() -> keyValueStore, () -> keyValueStore = new KeyValueStore(getAccountDatabase()));
+    }
+
     public ConfigurationStore getConfigurationStore() {
-        return configurationStore;
+        return getOrCreate(() -> configurationStore,
+                () -> configurationStore = new ConfigurationStore(getKeyValueStore()));
     }
 
     public MessageCache getMessageCache() {
@@ -1662,7 +1658,7 @@ public class SignalAccount implements Closeable {
     }
 
     public boolean isDiscoverableByPhoneNumber() {
-        final var phoneNumberUnlisted = configurationStore.getPhoneNumberUnlisted();
+        final var phoneNumberUnlisted = getConfigurationStore().getPhoneNumberUnlisted();
         return phoneNumberUnlisted == null || !phoneNumberUnlisted;
     }
 
index b810ea23a5c1eddc50b07c22e4abe8ea4997baed..f85d3d8084f37a6ce2ef9ed528ae3bcb8860913d 100644 (file)
 package org.asamk.signal.manager.storage.configuration;
 
 import org.asamk.signal.manager.api.PhoneNumberSharingMode;
+import org.asamk.signal.manager.storage.keyValue.KeyValueEntry;
+import org.asamk.signal.manager.storage.keyValue.KeyValueStore;
 
 public class ConfigurationStore {
 
-    private final Saver saver;
+    private final KeyValueStore keyValueStore;
 
-    private Boolean readReceipts;
-    private Boolean unidentifiedDeliveryIndicators;
-    private Boolean typingIndicators;
-    private Boolean linkPreviews;
-    private Boolean phoneNumberUnlisted;
-    private PhoneNumberSharingMode phoneNumberSharingMode;
+    private final KeyValueEntry<Boolean> readReceipts = new KeyValueEntry<>("config-read-receipts", Boolean.class);
+    private final KeyValueEntry<Boolean> unidentifiedDeliveryIndicators = new KeyValueEntry<>(
+            "config-unidentified-delivery-indicators",
+            Boolean.class);
+    private final KeyValueEntry<Boolean> typingIndicators = new KeyValueEntry<>("config-typing-indicators",
+            Boolean.class);
+    private final KeyValueEntry<Boolean> linkPreviews = new KeyValueEntry<>("config-link-previews", Boolean.class);
+    private final KeyValueEntry<Boolean> phoneNumberUnlisted = new KeyValueEntry<>("config-phone-number-unlisted",
+            Boolean.class);
+    private final KeyValueEntry<PhoneNumberSharingMode> phoneNumberSharingMode = new KeyValueEntry<>(
+            "config-phone-number-sharing-mode",
+            PhoneNumberSharingMode.class);
 
-    public ConfigurationStore(final Saver saver) {
-        this.saver = saver;
-    }
-
-    public static ConfigurationStore fromStorage(Storage storage, Saver saver) {
-        final var store = new ConfigurationStore(saver);
-        store.readReceipts = storage.readReceipts;
-        store.unidentifiedDeliveryIndicators = storage.unidentifiedDeliveryIndicators;
-        store.typingIndicators = storage.typingIndicators;
-        store.linkPreviews = storage.linkPreviews;
-        store.phoneNumberSharingMode = storage.phoneNumberSharingMode;
-        return store;
+    public ConfigurationStore(final KeyValueStore keyValueStore) {
+        this.keyValueStore = keyValueStore;
     }
 
     public Boolean getReadReceipts() {
-        return readReceipts;
+        return keyValueStore.getEntry(readReceipts);
     }
 
-    public void setReadReceipts(final boolean readReceipts) {
-        this.readReceipts = readReceipts;
-        saver.save(toStorage());
+    public void setReadReceipts(final boolean value) {
+        keyValueStore.storeEntry(readReceipts, value);
     }
 
     public Boolean getUnidentifiedDeliveryIndicators() {
-        return unidentifiedDeliveryIndicators;
+        return keyValueStore.getEntry(unidentifiedDeliveryIndicators);
     }
 
-    public void setUnidentifiedDeliveryIndicators(final boolean unidentifiedDeliveryIndicators) {
-        this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators;
-        saver.save(toStorage());
+    public void setUnidentifiedDeliveryIndicators(final boolean value) {
+        keyValueStore.storeEntry(unidentifiedDeliveryIndicators, value);
     }
 
     public Boolean getTypingIndicators() {
-        return typingIndicators;
+        return keyValueStore.getEntry(typingIndicators);
     }
 
-    public void setTypingIndicators(final boolean typingIndicators) {
-        this.typingIndicators = typingIndicators;
-        saver.save(toStorage());
+    public void setTypingIndicators(final boolean value) {
+        keyValueStore.storeEntry(typingIndicators, value);
     }
 
     public Boolean getLinkPreviews() {
-        return linkPreviews;
+        return keyValueStore.getEntry(linkPreviews);
     }
 
-    public void setLinkPreviews(final boolean linkPreviews) {
-        this.linkPreviews = linkPreviews;
-        saver.save(toStorage());
+    public void setLinkPreviews(final boolean value) {
+        keyValueStore.storeEntry(linkPreviews, value);
     }
 
     public Boolean getPhoneNumberUnlisted() {
-        return phoneNumberUnlisted;
+        return keyValueStore.getEntry(phoneNumberUnlisted);
     }
 
-    public void setPhoneNumberUnlisted(final boolean phoneNumberUnlisted) {
-        this.phoneNumberUnlisted = phoneNumberUnlisted;
-        saver.save(toStorage());
+    public void setPhoneNumberUnlisted(final boolean value) {
+        keyValueStore.storeEntry(phoneNumberUnlisted, value);
     }
 
     public PhoneNumberSharingMode getPhoneNumberSharingMode() {
-        return phoneNumberSharingMode;
-    }
-
-    public void setPhoneNumberSharingMode(final PhoneNumberSharingMode phoneNumberSharingMode) {
-        this.phoneNumberSharingMode = phoneNumberSharingMode;
-        saver.save(toStorage());
+        return keyValueStore.getEntry(phoneNumberSharingMode);
     }
 
-    private Storage toStorage() {
-        return new Storage(readReceipts,
-                unidentifiedDeliveryIndicators,
-                typingIndicators,
-                linkPreviews,
-                phoneNumberUnlisted,
-                phoneNumberSharingMode);
-    }
-
-    public record Storage(
-            Boolean readReceipts,
-            Boolean unidentifiedDeliveryIndicators,
-            Boolean typingIndicators,
-            Boolean linkPreviews,
-            Boolean phoneNumberUnlisted,
-            PhoneNumberSharingMode phoneNumberSharingMode
-    ) {}
-
-    public interface Saver {
-
-        void save(Storage storage);
+    public void setPhoneNumberSharingMode(final PhoneNumberSharingMode value) {
+        keyValueStore.storeEntry(phoneNumberSharingMode, value);
     }
 }
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/configuration/LegacyConfigurationStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/configuration/LegacyConfigurationStore.java
new file mode 100644 (file)
index 0000000..7c799fe
--- /dev/null
@@ -0,0 +1,33 @@
+package org.asamk.signal.manager.storage.configuration;
+
+import org.asamk.signal.manager.api.PhoneNumberSharingMode;
+
+public class LegacyConfigurationStore {
+
+    public static void migrate(Storage storage, ConfigurationStore configurationStore) {
+        if (storage.readReceipts != null) {
+            configurationStore.setReadReceipts(storage.readReceipts);
+        }
+        if (storage.unidentifiedDeliveryIndicators != null) {
+            configurationStore.setUnidentifiedDeliveryIndicators(storage.unidentifiedDeliveryIndicators);
+        }
+        if (storage.typingIndicators != null) {
+            configurationStore.setTypingIndicators(storage.typingIndicators);
+        }
+        if (storage.linkPreviews != null) {
+            configurationStore.setLinkPreviews(storage.linkPreviews);
+        }
+        if (storage.phoneNumberSharingMode != null) {
+            configurationStore.setPhoneNumberSharingMode(storage.phoneNumberSharingMode);
+        }
+    }
+
+    public record Storage(
+            Boolean readReceipts,
+            Boolean unidentifiedDeliveryIndicators,
+            Boolean typingIndicators,
+            Boolean linkPreviews,
+            Boolean phoneNumberUnlisted,
+            PhoneNumberSharingMode phoneNumberSharingMode
+    ) {}
+}
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueEntry.java b/lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueEntry.java
new file mode 100644 (file)
index 0000000..3195bc8
--- /dev/null
@@ -0,0 +1,8 @@
+package org.asamk.signal.manager.storage.keyValue;
+
+public record KeyValueEntry<T>(String key, Class<T> clazz, T defaultValue) {
+
+    public KeyValueEntry(String key, Class<T> clazz) {
+        this(key, clazz, null);
+    }
+}
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueStore.java
new file mode 100644 (file)
index 0000000..5785dfd
--- /dev/null
@@ -0,0 +1,153 @@
+package org.asamk.signal.manager.storage.keyValue;
+
+import org.asamk.signal.manager.storage.Database;
+import org.asamk.signal.manager.storage.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+
+public class KeyValueStore {
+
+    private static final String TABLE_KEY_VALUE = "key_value";
+    private final static Logger logger = LoggerFactory.getLogger(KeyValueStore.class);
+
+    private final Database database;
+
+    public static void createSql(Connection connection) throws SQLException {
+        // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
+        try (final var statement = connection.createStatement()) {
+            statement.executeUpdate("""
+                                    CREATE TABLE key_value (
+                                      _id INTEGER PRIMARY KEY,
+                                      key TEXT UNIQUE NOT NULL,
+                                      value ANY
+                                    ) STRICT;
+                                    """);
+        }
+    }
+
+    public KeyValueStore(final Database database) {
+        this.database = database;
+    }
+
+    public <T> T getEntry(KeyValueEntry<T> key) {
+        final var sql = (
+                """
+                SELECT key, value
+                FROM %s p
+                WHERE p.key = ?
+                """
+        ).formatted(TABLE_KEY_VALUE);
+        try (final var connection = database.getConnection()) {
+            try (final var statement = connection.prepareStatement(sql)) {
+                statement.setString(1, key.key());
+
+                final var result = Utils.executeQueryForOptional(statement,
+                        resultSet -> readValueFromResultSet(key, resultSet)).orElse(null);
+
+                if (result == null) {
+                    return key.defaultValue();
+                }
+                return result;
+            }
+        } catch (SQLException e) {
+            throw new RuntimeException("Failed read from pre_key store", e);
+        }
+    }
+
+    public <T> void storeEntry(KeyValueEntry<T> key, T value) {
+        final var sql = (
+                """
+                INSERT INTO %s (key, value)
+                VALUES (?1, ?2)
+                ON CONFLICT (key) DO UPDATE SET value=excluded.value
+                """
+        ).formatted(TABLE_KEY_VALUE);
+        try (final var connection = database.getConnection()) {
+            try (final var statement = connection.prepareStatement(sql)) {
+                statement.setString(1, key.key());
+                setParameterValue(statement, 2, key.clazz(), value);
+                statement.executeUpdate();
+            }
+        } catch (SQLException e) {
+            throw new RuntimeException("Failed update key_value store", e);
+        }
+    }
+
+    private static <T> T readValueFromResultSet(
+            final KeyValueEntry<T> key, final ResultSet resultSet
+    ) throws SQLException {
+        Object value;
+        final var clazz = key.clazz();
+        if (clazz == int.class || clazz == Integer.class) {
+            value = resultSet.getInt("value");
+        } else if (clazz == long.class || clazz == Long.class) {
+            value = resultSet.getLong("value");
+        } else if (clazz == boolean.class || clazz == Boolean.class) {
+            value = resultSet.getBoolean("value");
+        } else if (clazz == String.class) {
+            value = resultSet.getString("value");
+        } else if (Enum.class.isAssignableFrom(clazz)) {
+            final var name = resultSet.getString("value");
+            if (name == null) {
+                value = null;
+            } else {
+                try {
+                    value = Enum.valueOf((Class<Enum>) key.clazz(), name);
+                } catch (IllegalArgumentException e) {
+                    logger.debug("Read invalid enum value from store, ignoring: {} for {}", name, key.clazz());
+                    value = null;
+                }
+            }
+        } else {
+            throw new AssertionError("Invalid key type " + clazz.getSimpleName());
+        }
+        if (resultSet.wasNull()) {
+            return null;
+        }
+        return (T) value;
+    }
+
+    private static <T> void setParameterValue(
+            final PreparedStatement statement, final int parameterIndex, final Class<T> clazz, final T value
+    ) throws SQLException {
+        if (clazz == int.class || clazz == Integer.class) {
+            if (value == null) {
+                statement.setNull(parameterIndex, Types.INTEGER);
+            } else {
+                statement.setInt(parameterIndex, (int) value);
+            }
+        } else if (clazz == long.class || clazz == Long.class) {
+            if (value == null) {
+                statement.setNull(parameterIndex, Types.INTEGER);
+            } else {
+                statement.setLong(parameterIndex, (long) value);
+            }
+        } else if (clazz == boolean.class || clazz == Boolean.class) {
+            if (value == null) {
+                statement.setNull(parameterIndex, Types.BOOLEAN);
+            } else {
+                statement.setBoolean(parameterIndex, (boolean) value);
+            }
+        } else if (clazz == String.class) {
+            if (value == null) {
+                statement.setNull(parameterIndex, Types.VARCHAR);
+            } else {
+                statement.setString(parameterIndex, (String) value);
+            }
+        } else if (Enum.class.isAssignableFrom(clazz)) {
+            if (value == null) {
+                statement.setNull(parameterIndex, Types.VARCHAR);
+            } else {
+                statement.setString(parameterIndex, ((Enum<?>) value).name());
+            }
+        } else {
+            throw new AssertionError("Invalid key type " + clazz.getSimpleName());
+        }
+    }
+}