mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-08-13 03:26:02 +00:00
Implement sticker pack sync
This commit is contained in:
parent
151b42c6e9
commit
2cf5e25e9e
@ -4,13 +4,16 @@ import org.asamk.signal.manager.api.GroupIdV1;
|
||||
import org.asamk.signal.manager.api.GroupIdV2;
|
||||
import org.asamk.signal.manager.api.Pair;
|
||||
import org.asamk.signal.manager.api.Profile;
|
||||
import org.asamk.signal.manager.api.StickerPackId;
|
||||
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.asamk.signal.manager.storage.stickers.StickerPack;
|
||||
import org.asamk.signal.manager.syncStorage.AccountRecordProcessor;
|
||||
import org.asamk.signal.manager.syncStorage.ContactRecordProcessor;
|
||||
import org.asamk.signal.manager.syncStorage.GroupV1RecordProcessor;
|
||||
import org.asamk.signal.manager.syncStorage.GroupV2RecordProcessor;
|
||||
import org.asamk.signal.manager.syncStorage.StickerPackRecordProcessor;
|
||||
import org.asamk.signal.manager.syncStorage.StorageSyncModels;
|
||||
import org.asamk.signal.manager.syncStorage.StorageSyncValidations;
|
||||
import org.asamk.signal.manager.syncStorage.WriteOperationResult;
|
||||
@ -52,7 +55,8 @@ public class StorageHelper {
|
||||
private static final List<Integer> KNOWN_TYPES = List.of(ManifestRecord.Identifier.Type.CONTACT.getValue(),
|
||||
ManifestRecord.Identifier.Type.GROUPV1.getValue(),
|
||||
ManifestRecord.Identifier.Type.GROUPV2.getValue(),
|
||||
ManifestRecord.Identifier.Type.ACCOUNT.getValue());
|
||||
ManifestRecord.Identifier.Type.ACCOUNT.getValue(),
|
||||
ManifestRecord.Identifier.Type.STICKER_PACK.getValue());
|
||||
|
||||
private final SignalAccount account;
|
||||
private final SignalDependencies dependencies;
|
||||
@ -120,6 +124,7 @@ public class StorageHelper {
|
||||
logger.trace("Adding missing storageIds to local data");
|
||||
account.getRecipientStore().setMissingStorageIds();
|
||||
account.getGroupStore().setMissingStorageIds();
|
||||
account.getStickerStore().setMissingStorageIds();
|
||||
|
||||
var needsMultiDeviceSync = false;
|
||||
|
||||
@ -222,11 +227,14 @@ public class StorageHelper {
|
||||
final var updated = account.getRecipientStore()
|
||||
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
|
||||
oldUnregisteredLocalOnlyIds);
|
||||
final var updatedStickers = account.getStickerStore()
|
||||
.removeStorageIdsFromLocalOnlyDeletedStickerPacks(connection, oldUnregisteredLocalOnlyIds);
|
||||
|
||||
if (updated > 0) {
|
||||
if (updated > 0 || updatedStickers > 0) {
|
||||
logger.warn(
|
||||
"Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
|
||||
updated);
|
||||
"Found {} recipients and {} sticker packs that were deleted remotely but only marked deleted locally. Removed those from local store.",
|
||||
updated,
|
||||
updatedStickers);
|
||||
}
|
||||
}
|
||||
|
||||
@ -366,6 +374,7 @@ public class StorageHelper {
|
||||
final Map<RecipientId, StorageId> newContactStorageIds;
|
||||
final Map<GroupIdV1, StorageId> newGroupV1StorageIds;
|
||||
final Map<GroupIdV2, StorageId> newGroupV2StorageIds;
|
||||
final Map<StickerPackId, StorageId> newStickerPackStorageIds;
|
||||
|
||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
@ -412,6 +421,19 @@ public class StorageHelper {
|
||||
new StorageRecord.Builder().groupV2(record).build()));
|
||||
}
|
||||
|
||||
final var stickerPacks = account.getStickerStore()
|
||||
.getStickerPacks(connection)
|
||||
.stream()
|
||||
.filter(pack -> pack.isInstalled() || pack.deletedTimestamp() > 0)
|
||||
.toList();
|
||||
newStickerPackStorageIds = generateStickerPackStorageIds(stickerPacks);
|
||||
for (final var stickerPack : stickerPacks) {
|
||||
final var storageId = newStickerPackStorageIds.get(stickerPack.packId());
|
||||
final var record = StorageSyncModels.localToRemoteRecord(stickerPack);
|
||||
newStorageRecords.add(new SignalStorageRecord(storageId,
|
||||
new StorageRecord.Builder().stickerPack(record).build()));
|
||||
}
|
||||
|
||||
connection.commit();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed to sync remote storage", e);
|
||||
@ -462,6 +484,7 @@ public class StorageHelper {
|
||||
connection.setAutoCommit(false);
|
||||
account.getRecipientStore().updateStorageIds(connection, newContactStorageIds);
|
||||
account.getGroupStore().updateStorageIds(connection, newGroupV1StorageIds, newGroupV2StorageIds);
|
||||
account.getStickerStore().updateStorageIds(connection, newStickerPackStorageIds);
|
||||
|
||||
// delete all unknown storage ids
|
||||
account.getUnknownStorageIdStore().deleteAllUnknownStorageIds(connection);
|
||||
@ -494,6 +517,14 @@ public class StorageHelper {
|
||||
_ -> StorageId.forGroupV2(KeyUtils.createRawStorageId())));
|
||||
}
|
||||
|
||||
private Map<StickerPackId, StorageId> generateStickerPackStorageIds(
|
||||
final List<StickerPack> stickerPacks
|
||||
) {
|
||||
return stickerPacks.stream()
|
||||
.collect(Collectors.toMap(stickerPack -> stickerPack.packId(),
|
||||
_ -> StorageId.forStickerPack(KeyUtils.createRawStorageId())));
|
||||
}
|
||||
|
||||
private void storeManifestLocally(
|
||||
final SignalStorageManifest remoteManifest
|
||||
) {
|
||||
@ -533,6 +564,7 @@ public class StorageHelper {
|
||||
storageIds.addAll(account.getUnknownStorageIdStore().getUnknownStorageIds(connection));
|
||||
storageIds.addAll(account.getGroupStore().getStorageIds(connection));
|
||||
storageIds.addAll(account.getRecipientStore().getStorageIds(connection));
|
||||
storageIds.addAll(account.getStickerStore().getStorageIds(connection));
|
||||
storageIds.add(account.getRecipientStore().getSelfStorageId(connection));
|
||||
return storageIds;
|
||||
}
|
||||
@ -581,6 +613,14 @@ public class StorageHelper {
|
||||
account.getUsernameLink());
|
||||
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().account(record).build());
|
||||
}
|
||||
case ManifestRecord.Identifier.Type.STICKER_PACK -> {
|
||||
final var stickerPack = account.getStickerStore().getStickerPack(connection, storageId);
|
||||
if (stickerPack == null) {
|
||||
throw new AssertionError("Missing local sticker pack model for storage id: " + storageId);
|
||||
}
|
||||
final var record = StorageSyncModels.localToRemoteRecord(stickerPack);
|
||||
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().stickerPack(record).build());
|
||||
}
|
||||
case null, default -> {
|
||||
throw new AssertionError("Got unknown local storage record type: " + storageId);
|
||||
}
|
||||
@ -646,6 +686,7 @@ public class StorageHelper {
|
||||
final var groupV1RecordProcessor = new GroupV1RecordProcessor(account, connection);
|
||||
final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection);
|
||||
final var contactRecordProcessor = new ContactRecordProcessor(account, connection, context.getJobExecutor());
|
||||
final var stickerPackRecordProcessor = new StickerPackRecordProcessor(account, connection);
|
||||
|
||||
for (final var record : records) {
|
||||
if (record.getProto().account != null) {
|
||||
@ -664,6 +705,10 @@ public class StorageHelper {
|
||||
logger.debug("Reading record {} of type contact", record.getId());
|
||||
contactRecordProcessor.process(StorageRecordConvertersKt.toSignalContactRecord(record.getProto().contact,
|
||||
record.getId()));
|
||||
} else if (record.getProto().stickerPack != null) {
|
||||
logger.debug("Reading record {} of type stickerPack", record.getId());
|
||||
stickerPackRecordProcessor.process(StorageRecordConvertersKt.toSignalStickerPackRecord(record.getProto().stickerPack,
|
||||
record.getId()));
|
||||
} else {
|
||||
unknownRecords.add(record.getId());
|
||||
}
|
||||
@ -672,6 +717,7 @@ public class StorageHelper {
|
||||
processedRecords.addAll(groupV1RecordProcessor.getUpdatedStorageIds());
|
||||
processedRecords.addAll(groupV2RecordProcessor.getUpdatedStorageIds());
|
||||
processedRecords.addAll(contactRecordProcessor.getUpdatedStorageIds());
|
||||
processedRecords.addAll(stickerPackRecordProcessor.getUpdatedStorageIds());
|
||||
|
||||
return new Pair<>(unknownRecords, processedRecords);
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ import java.util.UUID;
|
||||
public class AccountDatabase extends Database {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
||||
private static final long DATABASE_VERSION = 28;
|
||||
private static final long DATABASE_VERSION = 29;
|
||||
|
||||
private AccountDatabase(final HikariDataSource dataSource) {
|
||||
super(logger, DATABASE_VERSION, dataSource);
|
||||
@ -623,6 +623,18 @@ public class AccountDatabase extends Database {
|
||||
""");
|
||||
}
|
||||
}
|
||||
if (oldVersion < 29) {
|
||||
logger.debug("Updating database: Adding sticker storage sync columns");
|
||||
try (final var statement = connection.createStatement()) {
|
||||
statement.executeUpdate("""
|
||||
ALTER TABLE sticker ADD COLUMN position INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE sticker ADD COLUMN deleted_timestamp INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE sticker ADD COLUMN storage_id BLOB;
|
||||
ALTER TABLE sticker ADD COLUMN storage_record BLOB;
|
||||
CREATE UNIQUE INDEX sticker_storage_id_index ON sticker (storage_id);
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void createUuidMappingTable(
|
||||
|
||||
@ -1,10 +1,29 @@
|
||||
package org.asamk.signal.manager.storage.stickers;
|
||||
|
||||
import org.asamk.signal.manager.api.StickerPackId;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
|
||||
public record StickerPack(long internalId, StickerPackId packId, byte[] packKey, boolean isInstalled) {
|
||||
public record StickerPack(
|
||||
long internalId,
|
||||
StickerPackId packId,
|
||||
byte[] packKey,
|
||||
boolean isInstalled,
|
||||
int position,
|
||||
long deletedTimestamp,
|
||||
StorageId storageId,
|
||||
byte[] storageRecord
|
||||
) {
|
||||
|
||||
public StickerPack(
|
||||
final long internalId,
|
||||
final StickerPackId packId,
|
||||
final byte[] packKey,
|
||||
final boolean isInstalled
|
||||
) {
|
||||
this(internalId, packId, packKey, isInstalled, 0, 0, null, null);
|
||||
}
|
||||
|
||||
public StickerPack(final StickerPackId packId, final byte[] packKey) {
|
||||
this(-1, packId, packKey, false);
|
||||
this(-1, packId, packKey, false, 0, 0, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,14 +3,19 @@ package org.asamk.signal.manager.storage.stickers;
|
||||
import org.asamk.signal.manager.api.StickerPackId;
|
||||
import org.asamk.signal.manager.storage.Database;
|
||||
import org.asamk.signal.manager.storage.Utils;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStickerPackRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class StickerStore {
|
||||
|
||||
@ -27,7 +32,11 @@ public class StickerStore {
|
||||
_id INTEGER PRIMARY KEY,
|
||||
pack_id BLOB UNIQUE NOT NULL,
|
||||
pack_key BLOB NOT NULL,
|
||||
installed INTEGER NOT NULL DEFAULT FALSE
|
||||
installed INTEGER NOT NULL DEFAULT FALSE,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
deleted_timestamp INTEGER NOT NULL DEFAULT 0,
|
||||
storage_id BLOB UNIQUE,
|
||||
storage_record BLOB
|
||||
) STRICT;
|
||||
""");
|
||||
}
|
||||
@ -38,55 +47,103 @@ public class StickerStore {
|
||||
}
|
||||
|
||||
public List<StickerPack> getStickerPacks() {
|
||||
final var sql = (
|
||||
"""
|
||||
SELECT s._id, s.pack_id, s.pack_key, s.installed
|
||||
FROM %s s
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var connection = database.getConnection()) {
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
try (var result = Utils.executeQueryForStream(statement, this::getStickerPackFromResultSet)) {
|
||||
return result.toList();
|
||||
}
|
||||
}
|
||||
return getStickerPacks(connection);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed read from sticker store", e);
|
||||
}
|
||||
}
|
||||
|
||||
public StickerPack getStickerPack(StickerPackId packId) {
|
||||
public List<StickerPack> getStickerPacks(final Connection connection) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
SELECT s._id, s.pack_id, s.pack_key, s.installed
|
||||
SELECT s._id, s.pack_id, s.pack_key, s.installed, s.position, s.deleted_timestamp, s.storage_id, s.storage_record
|
||||
FROM %s s
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
try (var result = Utils.executeQueryForStream(statement, this::getStickerPackFromResultSet)) {
|
||||
return result.toList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public StickerPack getStickerPack(StickerPackId packId) {
|
||||
try (final var connection = database.getConnection()) {
|
||||
return getStickerPack(connection, packId);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed read from sticker store", e);
|
||||
}
|
||||
}
|
||||
|
||||
public StickerPack getStickerPack(Connection connection, StickerPackId packId) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
SELECT s._id, s.pack_id, s.pack_key, s.installed, s.position, s.deleted_timestamp, s.storage_id, s.storage_record
|
||||
FROM %s s
|
||||
WHERE s.pack_id = ?
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var connection = database.getConnection()) {
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, packId.serialize());
|
||||
return Utils.executeQueryForOptional(statement, this::getStickerPackFromResultSet).orElse(null);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed read from sticker store", e);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, packId.serialize());
|
||||
return Utils.executeQueryForOptional(statement, this::getStickerPackFromResultSet).orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
public StickerPack getStickerPack(Connection connection, StorageId storageId) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
SELECT s._id, s.pack_id, s.pack_key, s.installed, s.position, s.deleted_timestamp, s.storage_id, s.storage_record
|
||||
FROM %s s
|
||||
WHERE s.storage_id = ?
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, storageId.getRaw());
|
||||
return Utils.executeQueryForOptional(statement, this::getStickerPackFromResultSet).orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void addStickerPack(StickerPack stickerPack) {
|
||||
final var sql = (
|
||||
"""
|
||||
INSERT INTO %s (pack_id, pack_key, installed)
|
||||
VALUES (?, ?, ?)
|
||||
INSERT INTO %s (pack_id, pack_key, installed, position, deleted_timestamp, storage_id, storage_record)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var connection = database.getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
var storageId = stickerPack.storageId();
|
||||
if (storageId == null && (stickerPack.isInstalled() || stickerPack.deletedTimestamp() > 0)) {
|
||||
storageId = StorageId.forStickerPack(KeyUtils.createRawStorageId());
|
||||
}
|
||||
|
||||
final var position = stickerPack.isInstalled() ? Math.max(stickerPack.position(),
|
||||
getNextPosition(connection)) : 0;
|
||||
var deletedTimestamp = stickerPack.deletedTimestamp();
|
||||
if (!stickerPack.isInstalled() && deletedTimestamp == 0 && storageId != null) {
|
||||
deletedTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, stickerPack.packId().serialize());
|
||||
statement.setBytes(2, stickerPack.packKey());
|
||||
statement.setBoolean(3, stickerPack.isInstalled());
|
||||
statement.setInt(4, position);
|
||||
statement.setLong(5, deletedTimestamp);
|
||||
if (storageId == null) {
|
||||
statement.setNull(6, Types.BLOB);
|
||||
} else {
|
||||
statement.setBytes(6, storageId.getRaw());
|
||||
}
|
||||
if (stickerPack.storageRecord() == null) {
|
||||
statement.setNull(7, Types.BLOB);
|
||||
} else {
|
||||
statement.setBytes(7, stickerPack.storageRecord());
|
||||
}
|
||||
statement.executeUpdate();
|
||||
}
|
||||
connection.commit();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed update sticker store", e);
|
||||
}
|
||||
@ -96,28 +153,279 @@ public class StickerStore {
|
||||
final var sql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET installed = ?
|
||||
SET installed = ?, position = ?, deleted_timestamp = ?, storage_id = ?
|
||||
WHERE pack_id = ?
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var connection = database.getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
final var existing = getStickerPack(connection, stickerPackId);
|
||||
if (existing == null || existing.isInstalled() == installed) {
|
||||
connection.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
final var newStorageId = StorageId.forStickerPack(KeyUtils.createRawStorageId());
|
||||
final var position = installed ? getNextPosition(connection) : 0;
|
||||
final var deletedTimestamp = installed ? 0 : System.currentTimeMillis();
|
||||
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, stickerPackId.serialize());
|
||||
statement.setBoolean(2, installed);
|
||||
statement.setBoolean(1, installed);
|
||||
statement.setInt(2, position);
|
||||
statement.setLong(3, deletedTimestamp);
|
||||
statement.setBytes(4, newStorageId.getRaw());
|
||||
statement.setBytes(5, stickerPackId.serialize());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
connection.commit();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed update sticker store", e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<StorageId> getStorageIds(final Connection connection) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
SELECT s.storage_id
|
||||
FROM %s s
|
||||
WHERE s.storage_id IS NOT NULL
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
return Utils.executeQueryForStream(statement, this::getStorageIdFromResultSet).toList();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateStorageId(
|
||||
final Connection connection,
|
||||
final StickerPackId packId,
|
||||
final StorageId storageId
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET storage_id = ?
|
||||
WHERE pack_id = ?
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, storageId.getRaw());
|
||||
statement.setBytes(2, packId.serialize());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateStorageIds(
|
||||
final Connection connection,
|
||||
final Map<StickerPackId, StorageId> storageIdMap
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET storage_id = ?
|
||||
WHERE pack_id = ?
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
for (final var entry : storageIdMap.entrySet()) {
|
||||
statement.setBytes(1, entry.getValue().getRaw());
|
||||
statement.setBytes(2, entry.getKey().serialize());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public StorageId getStorageId(final Connection connection, final StickerPackId packId) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
SELECT s.storage_id
|
||||
FROM %s s
|
||||
WHERE s.pack_id = ? AND s.storage_id IS NOT NULL
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, packId.serialize());
|
||||
final var storageId = Utils.executeQueryForOptional(statement, this::getStorageIdFromResultSet);
|
||||
if (storageId.isPresent()) {
|
||||
return storageId.get();
|
||||
}
|
||||
}
|
||||
|
||||
final var newStorageId = StorageId.forStickerPack(KeyUtils.createRawStorageId());
|
||||
updateStorageId(connection, packId, newStorageId);
|
||||
return newStorageId;
|
||||
}
|
||||
|
||||
public void storeStorageRecord(
|
||||
final Connection connection,
|
||||
final StickerPackId packId,
|
||||
final StorageId storageId,
|
||||
final byte[] storageRecord
|
||||
) throws SQLException {
|
||||
final var clearSql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET storage_id = NULL
|
||||
WHERE storage_id = ?
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var statement = connection.prepareStatement(clearSql)) {
|
||||
statement.setBytes(1, storageId.getRaw());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
|
||||
final var updateSql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET storage_id = ?, storage_record = ?
|
||||
WHERE pack_id = ?
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var statement = connection.prepareStatement(updateSql)) {
|
||||
statement.setBytes(1, storageId.getRaw());
|
||||
if (storageRecord == null) {
|
||||
statement.setNull(2, Types.BLOB);
|
||||
} else {
|
||||
statement.setBytes(2, storageRecord);
|
||||
}
|
||||
statement.setBytes(3, packId.serialize());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setMissingStorageIds() {
|
||||
final var selectSql = (
|
||||
"""
|
||||
SELECT s.pack_id
|
||||
FROM %s s
|
||||
WHERE s.storage_id IS NULL AND (s.installed = TRUE OR s.deleted_timestamp > 0)
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
final var updateSql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET storage_id = ?
|
||||
WHERE pack_id = ?
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
|
||||
try (final var connection = database.getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
try (final var selectStatement = connection.prepareStatement(selectSql)) {
|
||||
final var packIds = Utils.executeQueryForStream(selectStatement,
|
||||
resultSet -> StickerPackId.deserialize(resultSet.getBytes("pack_id"))).toList();
|
||||
try (final var updateStatement = connection.prepareStatement(updateSql)) {
|
||||
for (final var packId : packIds) {
|
||||
updateStatement.setBytes(1, KeyUtils.createRawStorageId());
|
||||
updateStatement.setBytes(2, packId.serialize());
|
||||
updateStatement.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
connection.commit();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed update sticker store", e);
|
||||
}
|
||||
}
|
||||
|
||||
public int removeStorageIdsFromLocalOnlyDeletedStickerPacks(
|
||||
final Connection connection,
|
||||
final Collection<StorageId> storageIds
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET storage_id = NULL
|
||||
WHERE storage_id = ? AND installed = FALSE AND deleted_timestamp > 0
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
var count = 0;
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
for (final var storageId : storageIds) {
|
||||
statement.setBytes(1, storageId.getRaw());
|
||||
count += statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public void upsertFromStorageSync(
|
||||
final Connection connection,
|
||||
final SignalStickerPackRecord record
|
||||
) throws SQLException {
|
||||
final var remote = record.getProto();
|
||||
final var packId = StickerPackId.deserialize(remote.packId.toByteArray());
|
||||
final var deleted = remote.deletedAtTimestamp > 0;
|
||||
final var packKey = remote.packKey.toByteArray();
|
||||
final var storageRecord = remote.encode();
|
||||
|
||||
final var current = getStickerPack(connection, packId);
|
||||
|
||||
if (current == null) {
|
||||
final var insertSql = (
|
||||
"""
|
||||
INSERT INTO %s (pack_id, pack_key, installed, position, deleted_timestamp, storage_id, storage_record)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var statement = connection.prepareStatement(insertSql)) {
|
||||
statement.setBytes(1, packId.serialize());
|
||||
statement.setBytes(2, packKey);
|
||||
statement.setBoolean(3, !deleted);
|
||||
statement.setInt(4, deleted ? 0 : remote.position);
|
||||
statement.setLong(5, remote.deletedAtTimestamp);
|
||||
statement.setBytes(6, record.getId().getRaw());
|
||||
statement.setBytes(7, storageRecord);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (packKey.length > 0) {
|
||||
final var updateSql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET pack_key = ?, installed = ?, position = ?, deleted_timestamp = ?, storage_id = ?, storage_record = ?
|
||||
WHERE pack_id = ?
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var statement = connection.prepareStatement(updateSql)) {
|
||||
statement.setBytes(1, packKey);
|
||||
statement.setBoolean(2, !deleted);
|
||||
statement.setInt(3, deleted ? 0 : remote.position);
|
||||
statement.setLong(4, remote.deletedAtTimestamp);
|
||||
statement.setBytes(5, record.getId().getRaw());
|
||||
statement.setBytes(6, storageRecord);
|
||||
statement.setBytes(7, packId.serialize());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} else {
|
||||
final var updateSql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET installed = ?, position = ?, deleted_timestamp = ?, storage_id = ?, storage_record = ?
|
||||
WHERE pack_id = ?
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var statement = connection.prepareStatement(updateSql)) {
|
||||
statement.setBoolean(1, !deleted);
|
||||
statement.setInt(2, deleted ? 0 : remote.position);
|
||||
statement.setLong(3, remote.deletedAtTimestamp);
|
||||
statement.setBytes(4, record.getId().getRaw());
|
||||
statement.setBytes(5, storageRecord);
|
||||
statement.setBytes(6, packId.serialize());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addLegacyStickers(Collection<StickerPack> stickerPacks) {
|
||||
logger.debug("Migrating legacy stickers to database");
|
||||
long start = System.nanoTime();
|
||||
final var sql = (
|
||||
"""
|
||||
INSERT INTO %s (pack_id, pack_key, installed)
|
||||
VALUES (?, ?, ?)
|
||||
INSERT INTO %s (pack_id, pack_key, installed, position, deleted_timestamp, storage_id, storage_record)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var connection = database.getConnection()) {
|
||||
@ -125,11 +433,23 @@ public class StickerStore {
|
||||
try (final var statement = connection.prepareStatement("DELETE FROM %s".formatted(TABLE_STICKER))) {
|
||||
statement.executeUpdate();
|
||||
}
|
||||
var installedPosition = 0;
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
for (final var sticker : stickerPacks) {
|
||||
final var storageId = sticker.isInstalled()
|
||||
? StorageId.forStickerPack(KeyUtils.createRawStorageId())
|
||||
: null;
|
||||
statement.setBytes(1, sticker.packId().serialize());
|
||||
statement.setBytes(2, sticker.packKey());
|
||||
statement.setBoolean(3, sticker.isInstalled());
|
||||
statement.setInt(4, sticker.isInstalled() ? installedPosition++ : 0);
|
||||
statement.setLong(5, 0);
|
||||
if (storageId == null) {
|
||||
statement.setNull(6, Types.BLOB);
|
||||
} else {
|
||||
statement.setBytes(6, storageId.getRaw());
|
||||
}
|
||||
statement.setNull(7, Types.BLOB);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
@ -145,6 +465,36 @@ public class StickerStore {
|
||||
final var packId = resultSet.getBytes("pack_id");
|
||||
final var packKey = resultSet.getBytes("pack_key");
|
||||
final var installed = resultSet.getBoolean("installed");
|
||||
return new StickerPack(internalId, StickerPackId.deserialize(packId), packKey, installed);
|
||||
final var position = resultSet.getInt("position");
|
||||
final var deletedTimestamp = resultSet.getLong("deleted_timestamp");
|
||||
final var storageIdBytes = resultSet.getBytes("storage_id");
|
||||
final var storageId = storageIdBytes == null ? null : StorageId.forStickerPack(storageIdBytes);
|
||||
final var storageRecord = resultSet.getBytes("storage_record");
|
||||
return new StickerPack(internalId,
|
||||
StickerPackId.deserialize(packId),
|
||||
packKey,
|
||||
installed,
|
||||
position,
|
||||
deletedTimestamp,
|
||||
storageId,
|
||||
storageRecord);
|
||||
}
|
||||
|
||||
private StorageId getStorageIdFromResultSet(final ResultSet resultSet) throws SQLException {
|
||||
final var storageId = resultSet.getBytes("storage_id");
|
||||
return StorageId.forStickerPack(storageId);
|
||||
}
|
||||
|
||||
private int getNextPosition(final Connection connection) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
SELECT IFNULL(MAX(position) + 1, 0) AS next_position
|
||||
FROM %s
|
||||
WHERE installed = TRUE
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
return Utils.executeQuerySingleRow(statement, resultSet -> resultSet.getInt("next_position"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
package org.asamk.signal.manager.syncStorage;
|
||||
|
||||
import org.asamk.signal.manager.api.StickerPackId;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStickerPackRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Optional;
|
||||
|
||||
public class StickerPackRecordProcessor extends DefaultStorageRecordProcessor<SignalStickerPackRecord> {
|
||||
|
||||
private static final int PACK_ID_LENGTH = 16;
|
||||
private static final int PACK_KEY_LENGTH = 32;
|
||||
|
||||
private final SignalAccount account;
|
||||
private final Connection connection;
|
||||
|
||||
public StickerPackRecordProcessor(final SignalAccount account, final Connection connection) {
|
||||
this.account = account;
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(final SignalStickerPackRecord lhs, final SignalStickerPackRecord rhs) {
|
||||
return lhs.getProto().packId.equals(rhs.getProto().packId) ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isInvalid(final SignalStickerPackRecord remote) {
|
||||
return remote.getProto().packId.size() != PACK_ID_LENGTH || (
|
||||
remote.getProto().deletedAtTimestamp == 0
|
||||
&& remote.getProto().packKey.size() != PACK_KEY_LENGTH
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<SignalStickerPackRecord> getMatching(final SignalStickerPackRecord remote) throws SQLException {
|
||||
final var packId = StickerPackId.deserialize(remote.getProto().packId.toByteArray());
|
||||
final var local = account.getStickerStore().getStickerPack(connection, packId);
|
||||
|
||||
if (local == null || (!local.isInstalled() && local.deletedTimestamp() == 0)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final StorageId storageId;
|
||||
if (local.storageId() != null) {
|
||||
storageId = local.storageId();
|
||||
} else {
|
||||
storageId = StorageId.forStickerPack(KeyUtils.createRawStorageId());
|
||||
account.getStickerStore().updateStorageId(connection, packId, storageId);
|
||||
}
|
||||
|
||||
return Optional.of(new SignalStickerPackRecord(storageId, StorageSyncModels.localToRemoteRecord(local)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SignalStickerPackRecord merge(
|
||||
final SignalStickerPackRecord remoteRecord,
|
||||
final SignalStickerPackRecord localRecord
|
||||
) {
|
||||
final var remote = remoteRecord.getProto();
|
||||
final var local = localRecord.getProto();
|
||||
|
||||
final var isRemoteDeleted = remote.deletedAtTimestamp > 0;
|
||||
final var isLocalDeleted = local.deletedAtTimestamp > 0;
|
||||
|
||||
if (isRemoteDeleted && isLocalDeleted && local.deletedAtTimestamp > remote.deletedAtTimestamp) {
|
||||
return localRecord;
|
||||
}
|
||||
|
||||
return remoteRecord;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void insertLocal(final SignalStickerPackRecord record) throws SQLException {
|
||||
account.getStickerStore().upsertFromStorageSync(connection, record);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateLocal(final StorageRecordUpdate<SignalStickerPackRecord> update) throws SQLException {
|
||||
account.getStickerStore().upsertFromStorageSync(connection, update.newRecord());
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
||||
import org.asamk.signal.manager.storage.identities.IdentityInfo;
|
||||
import org.asamk.signal.manager.storage.recipients.Recipient;
|
||||
import org.asamk.signal.manager.storage.stickers.StickerPack;
|
||||
import org.signal.core.models.ServiceId.ACI;
|
||||
import org.signal.core.models.ServiceId.PNI;
|
||||
import org.signal.core.util.UuidUtil;
|
||||
@ -16,12 +17,14 @@ import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStickerPackRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord.UsernameLink;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StickerPackRecord;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
@ -162,6 +165,23 @@ public final class StorageSyncModels {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static StickerPackRecord localToRemoteRecord(StickerPack stickerPack) {
|
||||
final var builder = SignalStickerPackRecord.Companion.newBuilder(stickerPack.storageRecord());
|
||||
builder.packId(ByteString.of(stickerPack.packId().serialize()));
|
||||
|
||||
if (stickerPack.deletedTimestamp() > 0) {
|
||||
builder.packKey(ByteString.EMPTY);
|
||||
builder.position(0);
|
||||
builder.deletedAtTimestamp(stickerPack.deletedTimestamp());
|
||||
} else {
|
||||
builder.packKey(ByteString.of(stickerPack.packKey()));
|
||||
builder.position(stickerPack.position());
|
||||
builder.deletedAtTimestamp(0);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static TrustLevel remoteToLocal(IdentityState identityState) {
|
||||
return switch (identityState) {
|
||||
case DEFAULT -> TrustLevel.TRUSTED_UNVERIFIED;
|
||||
|
||||
@ -9708,6 +9708,29 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "org.whispersystems.signalservice.internal.storage.protos.StickerPackRecord",
|
||||
"fields": [
|
||||
{
|
||||
"name": "deletedAtTimestamp"
|
||||
},
|
||||
{
|
||||
"name": "packId"
|
||||
},
|
||||
{
|
||||
"name": "packKey"
|
||||
},
|
||||
{
|
||||
"name": "position"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "org.whispersystems.signalservice.internal.storage.protos.StickerPackRecord$Builder"
|
||||
},
|
||||
{
|
||||
"type": "org.whispersystems.signalservice.internal.storage.protos.StickerPackRecord$Companion"
|
||||
},
|
||||
{
|
||||
"type": "org.whispersystems.signalservice.internal.storage.protos.StorageItem",
|
||||
"fields": [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user