"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,
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);
""");
}
}
+ 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;
+ """);
+ }
+ }
}
}
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;
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;
private RecipientStore recipientStore;
private StickerStore stickerStore;
private ConfigurationStore configurationStore;
- private ConfigurationStore.Storage configurationStoreStorage;
+ private KeyValueStore keyValueStore;
private MessageCache messageCache;
private MessageSendLogStore messageSendLogStore;
signalAccount.aciAccountData.setLocalRegistrationId(registrationId);
signalAccount.pniAccountData.setLocalRegistrationId(pniRegistrationId);
signalAccount.settings = settings;
- signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore);
signalAccount.registered = false;
pniIdentityKey,
profileKey);
- signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore);
-
signalAccount.getRecipientTrustedResolver()
.resolveSelfRecipientTrusted(signalAccount.getSelfRecipientAddress());
signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION;
}
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;
return false;
}
- private void saveConfigurationStore(ConfigurationStore.Storage storage) {
- this.configurationStoreStorage = storage;
- save();
- }
-
private void save() {
synchronized (fileChannel) {
var rootNode = jsonProcessor.createObjectNode();
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
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() {
}
public boolean isDiscoverableByPhoneNumber() {
- final var phoneNumberUnlisted = configurationStore.getPhoneNumberUnlisted();
+ final var phoneNumberUnlisted = getConfigurationStore().getPhoneNumberUnlisted();
return phoneNumberUnlisted == null || !phoneNumberUnlisted;
}
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);
}
}
--- /dev/null
+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
+ ) {}
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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());
+ }
+ }
+}