mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Move recipient store to database
This commit is contained in:
parent
9d534dc7bb
commit
862c2fec87
11 changed files with 1041 additions and 593 deletions
|
@ -1133,6 +1133,34 @@
|
||||||
"name":"org.asamk.signal.manager.storage.recipients.LegacyRecipientStore$RecipientStoreDeserializer",
|
"name":"org.asamk.signal.manager.storage.recipients.LegacyRecipientStore$RecipientStoreDeserializer",
|
||||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"org.asamk.signal.manager.storage.recipients.LegacyRecipientStore2$Storage",
|
||||||
|
"allDeclaredFields":true,
|
||||||
|
"queryAllDeclaredMethods":true,
|
||||||
|
"queryAllDeclaredConstructors":true,
|
||||||
|
"methods":[{"name":"<init>","parameterTypes":["java.util.List","long"] }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.asamk.signal.manager.storage.recipients.LegacyRecipientStore2$Storage$Recipient",
|
||||||
|
"allDeclaredFields":true,
|
||||||
|
"queryAllDeclaredMethods":true,
|
||||||
|
"queryAllDeclaredConstructors":true,
|
||||||
|
"methods":[{"name":"<init>","parameterTypes":["long","java.lang.String","java.lang.String","java.lang.String","java.lang.String","org.asamk.signal.manager.storage.recipients.LegacyRecipientStore2$Storage$Recipient$Contact","org.asamk.signal.manager.storage.recipients.LegacyRecipientStore2$Storage$Recipient$Profile"] }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.asamk.signal.manager.storage.recipients.LegacyRecipientStore2$Storage$Recipient$Contact",
|
||||||
|
"allDeclaredFields":true,
|
||||||
|
"queryAllDeclaredMethods":true,
|
||||||
|
"queryAllDeclaredConstructors":true,
|
||||||
|
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","int","boolean","boolean","boolean"] }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.asamk.signal.manager.storage.recipients.LegacyRecipientStore2$Storage$Recipient$Profile",
|
||||||
|
"allDeclaredFields":true,
|
||||||
|
"queryAllDeclaredMethods":true,
|
||||||
|
"queryAllDeclaredConstructors":true,
|
||||||
|
"methods":[{"name":"<init>","parameterTypes":["long","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.util.Set"] }]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"org.asamk.signal.manager.storage.recipients.RecipientAddress",
|
"name":"org.asamk.signal.manager.storage.recipients.RecipientAddress",
|
||||||
"allDeclaredFields":true,
|
"allDeclaredFields":true,
|
||||||
|
@ -1143,30 +1171,6 @@
|
||||||
{"name":"uuid","parameterTypes":[] }
|
{"name":"uuid","parameterTypes":[] }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name":"org.asamk.signal.manager.storage.recipients.RecipientStore$Storage",
|
|
||||||
"allDeclaredFields":true,
|
|
||||||
"allDeclaredMethods":true,
|
|
||||||
"allDeclaredConstructors":true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.asamk.signal.manager.storage.recipients.RecipientStore$Storage$Recipient",
|
|
||||||
"allDeclaredFields":true,
|
|
||||||
"allDeclaredMethods":true,
|
|
||||||
"allDeclaredConstructors":true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.asamk.signal.manager.storage.recipients.RecipientStore$Storage$Recipient$Contact",
|
|
||||||
"allDeclaredFields":true,
|
|
||||||
"allDeclaredMethods":true,
|
|
||||||
"allDeclaredConstructors":true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.asamk.signal.manager.storage.recipients.RecipientStore$Storage$Recipient$Profile",
|
|
||||||
"allDeclaredFields":true,
|
|
||||||
"allDeclaredMethods":true,
|
|
||||||
"allDeclaredConstructors":true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name":"org.asamk.signal.manager.storage.senderKeys.SenderKeySharedStore$Storage",
|
"name":"org.asamk.signal.manager.storage.senderKeys.SenderKeySharedStore$Storage",
|
||||||
"allDeclaredFields":true,
|
"allDeclaredFields":true,
|
||||||
|
|
|
@ -108,17 +108,12 @@ public final class ProfileHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ExpiringProfileKeyCredential> getExpiringProfileKeyCredential(List<RecipientId> recipientIds) {
|
public List<ExpiringProfileKeyCredential> getExpiringProfileKeyCredential(List<RecipientId> recipientIds) {
|
||||||
try {
|
|
||||||
account.getRecipientStore().setBulkUpdating(true);
|
|
||||||
final var profileFetches = Flowable.fromIterable(recipientIds)
|
final var profileFetches = Flowable.fromIterable(recipientIds)
|
||||||
.filter(recipientId -> !ExpiringProfileCredentialUtil.isValid(account.getProfileStore()
|
.filter(recipientId -> !ExpiringProfileCredentialUtil.isValid(account.getProfileStore()
|
||||||
.getExpiringProfileKeyCredential(recipientId)))
|
.getExpiringProfileKeyCredential(recipientId)))
|
||||||
.map(recipientId -> retrieveProfile(recipientId,
|
.map(recipientId -> retrieveProfile(recipientId,
|
||||||
SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL).onErrorComplete());
|
SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL).onErrorComplete());
|
||||||
Maybe.merge(profileFetches, 10).blockingSubscribe();
|
Maybe.merge(profileFetches, 10).blockingSubscribe();
|
||||||
} finally {
|
|
||||||
account.getRecipientStore().setBulkUpdating(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return recipientIds.stream().map(r -> account.getProfileStore().getExpiringProfileKeyCredential(r)).toList();
|
return recipientIds.stream().map(r -> account.getProfileStore().getExpiringProfileKeyCredential(r)).toList();
|
||||||
}
|
}
|
||||||
|
@ -233,16 +228,11 @@ public final class ProfileHelper {
|
||||||
|
|
||||||
private List<Profile> getRecipientProfiles(Collection<RecipientId> recipientIds, boolean force) {
|
private List<Profile> getRecipientProfiles(Collection<RecipientId> recipientIds, boolean force) {
|
||||||
final var profileStore = account.getProfileStore();
|
final var profileStore = account.getProfileStore();
|
||||||
try {
|
|
||||||
account.getRecipientStore().setBulkUpdating(true);
|
|
||||||
final var profileFetches = Flowable.fromIterable(recipientIds)
|
final var profileFetches = Flowable.fromIterable(recipientIds)
|
||||||
.filter(recipientId -> force || isProfileRefreshRequired(profileStore.getProfile(recipientId)))
|
.filter(recipientId -> force || isProfileRefreshRequired(profileStore.getProfile(recipientId)))
|
||||||
.map(recipientId -> retrieveProfile(recipientId,
|
.map(recipientId -> retrieveProfile(recipientId,
|
||||||
SignalServiceProfile.RequestType.PROFILE).onErrorComplete());
|
SignalServiceProfile.RequestType.PROFILE).onErrorComplete());
|
||||||
Maybe.merge(profileFetches, 10).blockingSubscribe();
|
Maybe.merge(profileFetches, 10).blockingSubscribe();
|
||||||
} finally {
|
|
||||||
account.getRecipientStore().setBulkUpdating(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return recipientIds.stream().map(profileStore::getProfile).toList();
|
return recipientIds.stream().map(profileStore::getProfile).toList();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.asamk.signal.manager.storage;
|
||||||
|
|
||||||
import com.zaxxer.hikari.HikariDataSource;
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientStore;
|
||||||
import org.asamk.signal.manager.storage.sendLog.MessageSendLogStore;
|
import org.asamk.signal.manager.storage.sendLog.MessageSendLogStore;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -13,7 +14,7 @@ import java.sql.SQLException;
|
||||||
public class AccountDatabase extends Database {
|
public class AccountDatabase extends Database {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
private final static Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
||||||
private static final long DATABASE_VERSION = 1;
|
private static final long DATABASE_VERSION = 2;
|
||||||
|
|
||||||
private AccountDatabase(final HikariDataSource dataSource) {
|
private AccountDatabase(final HikariDataSource dataSource) {
|
||||||
super(logger, DATABASE_VERSION, dataSource);
|
super(logger, DATABASE_VERSION, dataSource);
|
||||||
|
@ -24,10 +25,45 @@ public class AccountDatabase extends Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void upgradeDatabase(final Connection connection, final long oldVersion) throws SQLException {
|
protected void createDatabase(final Connection connection) throws SQLException {
|
||||||
if (oldVersion < 1) {
|
RecipientStore.createSql(connection);
|
||||||
logger.debug("Updating database: Creating message send log tables");
|
|
||||||
MessageSendLogStore.createSql(connection);
|
MessageSendLogStore.createSql(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void upgradeDatabase(final Connection connection, final long oldVersion) throws SQLException {
|
||||||
|
if (oldVersion < 2) {
|
||||||
|
logger.debug("Updating database: Creating recipient table");
|
||||||
|
try (final var statement = connection.createStatement()) {
|
||||||
|
statement.executeUpdate("""
|
||||||
|
CREATE TABLE recipient (
|
||||||
|
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
number TEXT UNIQUE,
|
||||||
|
uuid BLOB UNIQUE,
|
||||||
|
profile_key BLOB,
|
||||||
|
profile_key_credential BLOB,
|
||||||
|
|
||||||
|
given_name TEXT,
|
||||||
|
family_name TEXT,
|
||||||
|
color TEXT,
|
||||||
|
|
||||||
|
expiration_time INTEGER NOT NULL DEFAULT 0,
|
||||||
|
blocked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
archived BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
profile_sharing BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
|
||||||
|
profile_given_name TEXT,
|
||||||
|
profile_family_name TEXT,
|
||||||
|
profile_about TEXT,
|
||||||
|
profile_about_emoji TEXT,
|
||||||
|
profile_avatar_url_path TEXT,
|
||||||
|
profile_mobile_coin_address BLOB,
|
||||||
|
profile_unidentified_access_mode TEXT,
|
||||||
|
profile_capabilities TEXT
|
||||||
|
);
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,19 +53,26 @@ public abstract class Database implements AutoCloseable {
|
||||||
|
|
||||||
protected final void initDb() throws SQLException {
|
protected final void initDb() throws SQLException {
|
||||||
try (final var connection = dataSource.getConnection()) {
|
try (final var connection = dataSource.getConnection()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
final var userVersion = getUserVersion(connection);
|
final var userVersion = getUserVersion(connection);
|
||||||
logger.trace("Current database version: {} Program database version: {}", userVersion, databaseVersion);
|
logger.trace("Current database version: {} Program database version: {}", userVersion, databaseVersion);
|
||||||
|
|
||||||
if (userVersion > databaseVersion) {
|
if (userVersion == 0) {
|
||||||
|
createDatabase(connection);
|
||||||
|
setUserVersion(connection, databaseVersion);
|
||||||
|
} else if (userVersion > databaseVersion) {
|
||||||
logger.error("Database has been updated by a newer signal-cli version");
|
logger.error("Database has been updated by a newer signal-cli version");
|
||||||
throw new SQLException("Database has been updated by a newer signal-cli version");
|
throw new SQLException("Database has been updated by a newer signal-cli version");
|
||||||
} else if (userVersion < databaseVersion) {
|
} else if (userVersion < databaseVersion) {
|
||||||
upgradeDatabase(connection, userVersion);
|
upgradeDatabase(connection, userVersion);
|
||||||
setUserVersion(connection, databaseVersion);
|
setUserVersion(connection, databaseVersion);
|
||||||
}
|
}
|
||||||
|
connection.commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract void createDatabase(final Connection connection) throws SQLException;
|
||||||
|
|
||||||
protected abstract void upgradeDatabase(final Connection connection, long oldVersion) throws SQLException;
|
protected abstract void upgradeDatabase(final Connection connection, long oldVersion) throws SQLException;
|
||||||
|
|
||||||
private static long getUserVersion(final Connection connection) throws SQLException {
|
private static long getUserVersion(final Connection connection) throws SQLException {
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore;
|
||||||
import org.asamk.signal.manager.storage.protocol.SignalProtocolStore;
|
import org.asamk.signal.manager.storage.protocol.SignalProtocolStore;
|
||||||
import org.asamk.signal.manager.storage.recipients.Contact;
|
import org.asamk.signal.manager.storage.recipients.Contact;
|
||||||
import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore;
|
import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore2;
|
||||||
import org.asamk.signal.manager.storage.recipients.Profile;
|
import org.asamk.signal.manager.storage.recipients.Profile;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
@ -93,7 +94,7 @@ public class SignalAccount implements Closeable {
|
||||||
private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
||||||
|
|
||||||
private static final int MINIMUM_STORAGE_VERSION = 1;
|
private static final int MINIMUM_STORAGE_VERSION = 1;
|
||||||
private static final int CURRENT_STORAGE_VERSION = 4;
|
private static final int CURRENT_STORAGE_VERSION = 5;
|
||||||
|
|
||||||
private final Object LOCK = new Object();
|
private final Object LOCK = new Object();
|
||||||
|
|
||||||
|
@ -392,8 +393,6 @@ public class SignalAccount implements Closeable {
|
||||||
// Old config file, creating new profile key
|
// Old config file, creating new profile key
|
||||||
setProfileKey(KeyUtils.createProfileKey());
|
setProfileKey(KeyUtils.createProfileKey());
|
||||||
}
|
}
|
||||||
// Ensure our profile key is stored in profile store
|
|
||||||
getProfileStore().storeSelfProfileKey(getSelfRecipientId(), getProfileKey());
|
|
||||||
if (previousStorageVersion < 3) {
|
if (previousStorageVersion < 3) {
|
||||||
for (final var group : groupStore.getGroups()) {
|
for (final var group : groupStore.getGroups()) {
|
||||||
if (group instanceof GroupInfoV2 && group.getDistributionId() == null) {
|
if (group instanceof GroupInfoV2 && group.getDistributionId() == null) {
|
||||||
|
@ -514,9 +513,10 @@ public class SignalAccount implements Closeable {
|
||||||
if (rootNode.hasNonNull("version")) {
|
if (rootNode.hasNonNull("version")) {
|
||||||
var accountVersion = rootNode.get("version").asInt(1);
|
var accountVersion = rootNode.get("version").asInt(1);
|
||||||
if (accountVersion > CURRENT_STORAGE_VERSION) {
|
if (accountVersion > CURRENT_STORAGE_VERSION) {
|
||||||
throw new IOException("Config file was created by a more recent version!");
|
throw new IOException("Config file was created by a more recent version: " + accountVersion);
|
||||||
} else if (accountVersion < MINIMUM_STORAGE_VERSION) {
|
} else if (accountVersion < MINIMUM_STORAGE_VERSION) {
|
||||||
throw new IOException("Config file was created by a no longer supported older version!");
|
throw new IOException("Config file was created by a no longer supported older version: "
|
||||||
|
+ accountVersion);
|
||||||
}
|
}
|
||||||
previousStorageVersion = accountVersion;
|
previousStorageVersion = accountVersion;
|
||||||
if (accountVersion < CURRENT_STORAGE_VERSION) {
|
if (accountVersion < CURRENT_STORAGE_VERSION) {
|
||||||
|
@ -621,6 +621,15 @@ public class SignalAccount implements Closeable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (previousStorageVersion < 5) {
|
||||||
|
final var legacyRecipientsStoreFile = getRecipientsStoreFile(dataPath, accountPath);
|
||||||
|
if (legacyRecipientsStoreFile.exists()) {
|
||||||
|
LegacyRecipientStore2.migrate(legacyRecipientsStoreFile, getRecipientStore());
|
||||||
|
// Ensure our profile key is stored in profile store
|
||||||
|
getProfileStore().storeSelfProfileKey(getSelfRecipientId(), getProfileKey());
|
||||||
|
migratedLegacyConfig = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
final var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
|
final var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
|
||||||
? jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
|
? jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
|
||||||
LegacyJsonSignalProtocolStore.class)
|
LegacyJsonSignalProtocolStore.class)
|
||||||
|
@ -681,7 +690,8 @@ public class SignalAccount implements Closeable {
|
||||||
logger.debug("Migrating legacy recipient store.");
|
logger.debug("Migrating legacy recipient store.");
|
||||||
var legacyRecipientStore = jsonProcessor.convertValue(legacyRecipientStoreNode, LegacyRecipientStore.class);
|
var legacyRecipientStore = jsonProcessor.convertValue(legacyRecipientStoreNode, LegacyRecipientStore.class);
|
||||||
if (legacyRecipientStore != null) {
|
if (legacyRecipientStore != null) {
|
||||||
getRecipientStore().resolveRecipientsTrusted(legacyRecipientStore.getAddresses());
|
legacyRecipientStore.getAddresses()
|
||||||
|
.forEach(recipient -> getRecipientStore().resolveRecipientTrusted(recipient));
|
||||||
}
|
}
|
||||||
getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress());
|
getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress());
|
||||||
migrated = true;
|
migrated = true;
|
||||||
|
@ -1094,22 +1104,42 @@ public class SignalAccount implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientResolver getRecipientResolver() {
|
public RecipientResolver getRecipientResolver() {
|
||||||
return getRecipientStore();
|
return new RecipientResolver() {
|
||||||
|
@Override
|
||||||
|
public RecipientId resolveRecipient(final RecipientAddress address) {
|
||||||
|
return getRecipientStore().resolveRecipient(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecipientId resolveRecipient(final long recipientId) {
|
||||||
|
return getRecipientStore().resolveRecipient(recipientId);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientTrustedResolver getRecipientTrustedResolver() {
|
public RecipientTrustedResolver getRecipientTrustedResolver() {
|
||||||
return getRecipientStore();
|
return new RecipientTrustedResolver() {
|
||||||
|
@Override
|
||||||
|
public RecipientId resolveSelfRecipientTrusted(final RecipientAddress address) {
|
||||||
|
return getRecipientStore().resolveSelfRecipientTrusted(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecipientId resolveRecipientTrusted(final SignalServiceAddress address) {
|
||||||
|
return getRecipientStore().resolveRecipientTrusted(address);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientAddressResolver getRecipientAddressResolver() {
|
public RecipientAddressResolver getRecipientAddressResolver() {
|
||||||
return getRecipientStore()::resolveRecipientAddress;
|
return recipientId -> getRecipientStore().resolveRecipientAddress(recipientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientStore getRecipientStore() {
|
public RecipientStore getRecipientStore() {
|
||||||
return getOrCreate(() -> recipientStore,
|
return getOrCreate(() -> recipientStore,
|
||||||
() -> recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, accountPath),
|
() -> recipientStore = new RecipientStore(this::mergeRecipients,
|
||||||
this::mergeRecipients,
|
this::getSelfRecipientAddress,
|
||||||
this::getSelfRecipientAddress));
|
getAccountDatabase()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProfileStore getProfileStore() {
|
public ProfileStore getProfileStore() {
|
||||||
|
|
|
@ -10,13 +10,25 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
|
||||||
import java.io.InvalidObjectException;
|
import java.io.InvalidObjectException;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Spliterator;
|
||||||
|
import java.util.Spliterators;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(Utils.class);
|
||||||
|
|
||||||
private Utils() {
|
private Utils() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,4 +61,51 @@ public class Utils {
|
||||||
return new RecipientAddress(Optional.empty(), Optional.of(identifier));
|
return new RecipientAddress(Optional.empty(), Optional.of(identifier));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> T executeQuerySingleRow(
|
||||||
|
PreparedStatement statement, ResultSetMapper<T> mapper
|
||||||
|
) throws SQLException {
|
||||||
|
final var resultSet = statement.executeQuery();
|
||||||
|
if (!resultSet.next()) {
|
||||||
|
throw new RuntimeException("Expected a row in result set, but none found.");
|
||||||
|
}
|
||||||
|
return mapper.apply(resultSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Optional<T> executeQueryForOptional(
|
||||||
|
PreparedStatement statement, ResultSetMapper<T> mapper
|
||||||
|
) throws SQLException {
|
||||||
|
final var resultSet = statement.executeQuery();
|
||||||
|
if (!resultSet.next()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(mapper.apply(resultSet));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Stream<T> executeQueryForStream(
|
||||||
|
PreparedStatement statement, ResultSetMapper<T> mapper
|
||||||
|
) throws SQLException {
|
||||||
|
final var resultSet = statement.executeQuery();
|
||||||
|
|
||||||
|
return StreamSupport.stream(new Spliterators.AbstractSpliterator<>(Long.MAX_VALUE, Spliterator.ORDERED) {
|
||||||
|
@Override
|
||||||
|
public boolean tryAdvance(final Consumer<? super T> consumer) {
|
||||||
|
try {
|
||||||
|
if (!resultSet.next()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
consumer.accept(mapper.apply(resultSet));
|
||||||
|
return true;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.warn("Failed to read from database result", e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ResultSetMapper<T> {
|
||||||
|
|
||||||
|
T apply(ResultSet resultSet) throws SQLException;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ public interface ProfileStore {
|
||||||
void storeProfileKey(RecipientId recipientId, ProfileKey profileKey);
|
void storeProfileKey(RecipientId recipientId, ProfileKey profileKey);
|
||||||
|
|
||||||
void storeExpiringProfileKeyCredential(
|
void storeExpiringProfileKeyCredential(
|
||||||
RecipientId recipientId,
|
RecipientId recipientId, ExpiringProfileKeyCredential expiringProfileKeyCredential
|
||||||
ExpiringProfileKeyCredential expiringProfileKeyCredential
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
package org.asamk.signal.manager.storage.recipients;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.storage.Utils;
|
||||||
|
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||||
|
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||||
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class LegacyRecipientStore2 {
|
||||||
|
|
||||||
|
private final static Logger logger = LoggerFactory.getLogger(LegacyRecipientStore2.class);
|
||||||
|
|
||||||
|
public static void migrate(File file, RecipientStore recipientStore) {
|
||||||
|
final var objectMapper = Utils.createStorageObjectMapper();
|
||||||
|
try (var inputStream = new FileInputStream(file)) {
|
||||||
|
final var storage = objectMapper.readValue(inputStream, Storage.class);
|
||||||
|
|
||||||
|
final var recipients = storage.recipients.stream().map(r -> {
|
||||||
|
final var recipientId = new RecipientId(r.id, recipientStore);
|
||||||
|
final var address = new RecipientAddress(Optional.ofNullable(r.uuid).map(UuidUtil::parseOrThrow),
|
||||||
|
Optional.ofNullable(r.number));
|
||||||
|
|
||||||
|
Contact contact = null;
|
||||||
|
if (r.contact != null) {
|
||||||
|
contact = new Contact(r.contact.name,
|
||||||
|
null,
|
||||||
|
r.contact.color,
|
||||||
|
r.contact.messageExpirationTime,
|
||||||
|
r.contact.blocked,
|
||||||
|
r.contact.archived,
|
||||||
|
r.contact.profileSharingEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileKey profileKey = null;
|
||||||
|
if (r.profileKey != null) {
|
||||||
|
try {
|
||||||
|
profileKey = new ProfileKey(Base64.getDecoder().decode(r.profileKey));
|
||||||
|
} catch (InvalidInputException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpiringProfileKeyCredential expiringProfileKeyCredential = null;
|
||||||
|
if (r.expiringProfileKeyCredential != null) {
|
||||||
|
try {
|
||||||
|
expiringProfileKeyCredential = new ExpiringProfileKeyCredential(Base64.getDecoder()
|
||||||
|
.decode(r.expiringProfileKeyCredential));
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Profile profile = null;
|
||||||
|
if (r.profile != null) {
|
||||||
|
profile = new Profile(r.profile.lastUpdateTimestamp,
|
||||||
|
r.profile.givenName,
|
||||||
|
r.profile.familyName,
|
||||||
|
r.profile.about,
|
||||||
|
r.profile.aboutEmoji,
|
||||||
|
r.profile.avatarUrlPath,
|
||||||
|
r.profile.mobileCoinAddress == null
|
||||||
|
? null
|
||||||
|
: Base64.getDecoder().decode(r.profile.mobileCoinAddress),
|
||||||
|
Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode),
|
||||||
|
r.profile.capabilities.stream()
|
||||||
|
.map(Profile.Capability::valueOfOrNull)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Recipient(recipientId, address, contact, profileKey, expiringProfileKeyCredential, profile);
|
||||||
|
}).collect(Collectors.toMap(Recipient::getRecipientId, r -> r));
|
||||||
|
|
||||||
|
recipientStore.addLegacyRecipients(recipients);
|
||||||
|
Files.delete(file.toPath());
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
// nothing to migrate
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Failed to load recipient store", e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Storage(List<Recipient> recipients, long lastId) {
|
||||||
|
|
||||||
|
private record Recipient(
|
||||||
|
long id,
|
||||||
|
String number,
|
||||||
|
String uuid,
|
||||||
|
String profileKey,
|
||||||
|
String expiringProfileKeyCredential,
|
||||||
|
Contact contact,
|
||||||
|
Profile profile
|
||||||
|
) {
|
||||||
|
|
||||||
|
private record Contact(
|
||||||
|
String name,
|
||||||
|
String color,
|
||||||
|
int messageExpirationTime,
|
||||||
|
boolean blocked,
|
||||||
|
boolean archived,
|
||||||
|
boolean profileSharingEnabled
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private record Profile(
|
||||||
|
long lastUpdateTimestamp,
|
||||||
|
String givenName,
|
||||||
|
String familyName,
|
||||||
|
String about,
|
||||||
|
String aboutEmoji,
|
||||||
|
String avatarUrlPath,
|
||||||
|
String mobileCoinAddress,
|
||||||
|
String unidentifiedAccessMode,
|
||||||
|
Set<String> capabilities
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,24 @@
|
||||||
package org.asamk.signal.manager.storage.recipients;
|
package org.asamk.signal.manager.storage.recipients;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.storage.Utils;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
public interface RecipientResolver {
|
public interface RecipientResolver {
|
||||||
|
|
||||||
RecipientId resolveRecipient(String identifier);
|
|
||||||
|
|
||||||
RecipientId resolveRecipient(RecipientAddress address);
|
RecipientId resolveRecipient(RecipientAddress address);
|
||||||
|
|
||||||
RecipientId resolveRecipient(SignalServiceAddress address);
|
|
||||||
|
|
||||||
RecipientId resolveRecipient(ServiceId aci);
|
|
||||||
|
|
||||||
RecipientId resolveRecipient(long recipientId);
|
RecipientId resolveRecipient(long recipientId);
|
||||||
|
|
||||||
|
default RecipientId resolveRecipient(String identifier) {
|
||||||
|
return resolveRecipient(Utils.getRecipientAddressFromIdentifier(identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
default RecipientId resolveRecipient(SignalServiceAddress address) {
|
||||||
|
return resolveRecipient(new RecipientAddress(address));
|
||||||
|
}
|
||||||
|
|
||||||
|
default RecipientId resolveRecipient(ServiceId serviceId) {
|
||||||
|
return resolveRecipient(new RecipientAddress(serviceId.uuid()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,6 +3,7 @@ package org.asamk.signal.manager.storage.sendLog;
|
||||||
import org.asamk.signal.manager.groups.GroupId;
|
import org.asamk.signal.manager.groups.GroupId;
|
||||||
import org.asamk.signal.manager.groups.GroupUtils;
|
import org.asamk.signal.manager.groups.GroupUtils;
|
||||||
import org.asamk.signal.manager.storage.Database;
|
import org.asamk.signal.manager.storage.Database;
|
||||||
|
import org.asamk.signal.manager.storage.Utils;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||||
|
@ -15,18 +16,11 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
|
||||||
import java.sql.ResultSet;
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Spliterator;
|
|
||||||
import java.util.Spliterators;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
import java.util.stream.StreamSupport;
|
|
||||||
|
|
||||||
public class MessageSendLogStore implements AutoCloseable {
|
public class MessageSendLogStore implements AutoCloseable {
|
||||||
|
|
||||||
|
@ -68,12 +62,13 @@ public class MessageSendLogStore implements AutoCloseable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void createSql(Connection connection) throws SQLException {
|
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()) {
|
try (final var statement = connection.createStatement()) {
|
||||||
statement.executeUpdate("""
|
statement.executeUpdate("""
|
||||||
CREATE TABLE message_send_log (
|
CREATE TABLE message_send_log (
|
||||||
_id INTEGER PRIMARY KEY,
|
_id INTEGER PRIMARY KEY,
|
||||||
content_id INTEGER NOT NULL REFERENCES message_send_log_content (_id) ON DELETE CASCADE,
|
content_id INTEGER NOT NULL REFERENCES message_send_log_content (_id) ON DELETE CASCADE,
|
||||||
recipient_id INTEGER NOT NULL,
|
recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
|
||||||
device_id INTEGER NOT NULL
|
device_id INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
CREATE TABLE message_send_log_content (
|
CREATE TABLE message_send_log_content (
|
||||||
|
@ -106,7 +101,7 @@ public class MessageSendLogStore implements AutoCloseable {
|
||||||
statement.setLong(1, recipientId.id());
|
statement.setLong(1, recipientId.id());
|
||||||
statement.setInt(2, deviceId);
|
statement.setInt(2, deviceId);
|
||||||
statement.setLong(3, timestamp);
|
statement.setLong(3, timestamp);
|
||||||
try (var result = executeQueryForStream(statement, resultSet -> {
|
try (var result = Utils.executeQueryForStream(statement, resultSet -> {
|
||||||
final var groupId = Optional.ofNullable(resultSet.getBytes("group_id"))
|
final var groupId = Optional.ofNullable(resultSet.getBytes("group_id"))
|
||||||
.map(GroupId::unknownVersion);
|
.map(GroupId::unknownVersion);
|
||||||
final SignalServiceProtos.Content content;
|
final SignalServiceProtos.Content content;
|
||||||
|
@ -389,32 +384,5 @@ public class MessageSendLogStore implements AutoCloseable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> Stream<T> executeQueryForStream(
|
|
||||||
PreparedStatement statement, ResultSetMapper<T> mapper
|
|
||||||
) throws SQLException {
|
|
||||||
final var resultSet = statement.executeQuery();
|
|
||||||
|
|
||||||
return StreamSupport.stream(new Spliterators.AbstractSpliterator<>(Long.MAX_VALUE, Spliterator.ORDERED) {
|
|
||||||
@Override
|
|
||||||
public boolean tryAdvance(final Consumer<? super T> consumer) {
|
|
||||||
try {
|
|
||||||
if (!resultSet.next()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
consumer.accept(mapper.apply(resultSet));
|
|
||||||
return true;
|
|
||||||
} catch (SQLException e) {
|
|
||||||
logger.warn("Failed to read from database result", e);
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private interface ResultSetMapper<T> {
|
|
||||||
|
|
||||||
T apply(ResultSet resultSet) throws SQLException;
|
|
||||||
}
|
|
||||||
|
|
||||||
private record RecipientDevices(RecipientId recipientId, List<Integer> deviceIds) {}
|
private record RecipientDevices(RecipientId recipientId, List<Integer> deviceIds) {}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue