From: AsamK Date: Wed, 4 Oct 2023 17:44:45 +0000 (+0200) Subject: Move configuration store to db X-Git-Tag: v0.12.3~31 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/c0f771684d517bfd16ea5519dfb555da6a15e09e Move configuration store to db --- diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index 3125a44e..98af166e 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -1111,6 +1111,13 @@ "queryAllDeclaredConstructors":true, "methods":[{"name":"","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":"","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, diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java b/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java index fb34b440..d9839a18 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java @@ -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; + """); + } + } } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index aef1d1c9..16017d5e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -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; } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/configuration/ConfigurationStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/configuration/ConfigurationStore.java index b810ea23..f85d3d80 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/configuration/ConfigurationStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/configuration/ConfigurationStore.java @@ -1,106 +1,75 @@ 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 readReceipts = new KeyValueEntry<>("config-read-receipts", Boolean.class); + private final KeyValueEntry unidentifiedDeliveryIndicators = new KeyValueEntry<>( + "config-unidentified-delivery-indicators", + Boolean.class); + private final KeyValueEntry typingIndicators = new KeyValueEntry<>("config-typing-indicators", + Boolean.class); + private final KeyValueEntry linkPreviews = new KeyValueEntry<>("config-link-previews", Boolean.class); + private final KeyValueEntry phoneNumberUnlisted = new KeyValueEntry<>("config-phone-number-unlisted", + Boolean.class); + private final KeyValueEntry 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 index 00000000..7c799fe1 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/configuration/LegacyConfigurationStore.java @@ -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 index 00000000..3195bc81 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueEntry.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager.storage.keyValue; + +public record KeyValueEntry(String key, Class clazz, T defaultValue) { + + public KeyValueEntry(String key, Class 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 index 00000000..5785dfdc --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueStore.java @@ -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 getEntry(KeyValueEntry 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 void storeEntry(KeyValueEntry 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 readValueFromResultSet( + final KeyValueEntry 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) 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 void setParameterValue( + final PreparedStatement statement, final int parameterIndex, final Class 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()); + } + } +}