mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 02:20:39 +00:00
Move configuration store to db
This commit is contained in:
parent
90ec01bfbf
commit
c0f771684d
7 changed files with 264 additions and 86 deletions
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
) {}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue