Implement sticker pack sync

This commit is contained in:
AsamK 2026-07-29 22:25:13 +02:00
parent 151b42c6e9
commit 2cf5e25e9e
7 changed files with 592 additions and 36 deletions

View File

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

View File

@ -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(

View File

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

View File

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

View File

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

View File

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

View File

@ -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": [