Move configuration store to db

This commit is contained in:
AsamK 2023-10-04 19:44:45 +02:00
parent 90ec01bfbf
commit c0f771684d
7 changed files with 264 additions and 86 deletions

View file

@ -1111,6 +1111,13 @@
"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,

View 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;
""");
}
}
}
}

View 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;
}

View file

@ -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<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;
return keyValueStore.getEntry(phoneNumberSharingMode);
}
public void setPhoneNumberSharingMode(final PhoneNumberSharingMode phoneNumberSharingMode) {
this.phoneNumberSharingMode = phoneNumberSharingMode;
saver.save(toStorage());
}
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);
}
}

View file

@ -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
) {}
}

View file

@ -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);
}
}

View file

@ -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());
}
}
}