Refactor sticker store

This commit is contained in:
AsamK 2021-05-02 12:08:47 +02:00
parent 4e123a2dc3
commit 624fa4fda4
8 changed files with 214 additions and 171 deletions

View file

@ -39,6 +39,7 @@ import org.asamk.signal.manager.storage.recipients.Contact;
import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.stickers.Sticker;
import org.asamk.signal.manager.storage.stickers.StickerPackId;
import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.KeyUtils;
@ -1170,7 +1171,7 @@ public class Manager implements Closeable {
var packKey = KeyUtils.createStickerUploadKey();
var packId = messageSender.uploadStickerManifest(manifest, packKey);
var sticker = new Sticker(Hex.fromStringCondensed(packId), packKey);
var sticker = new Sticker(StickerPackId.deserialize(Hex.fromStringCondensed(packId)), packKey);
account.getStickerStore().updateSticker(sticker);
account.save();
@ -1591,9 +1592,10 @@ public class Manager implements Closeable {
}
if (message.getSticker().isPresent()) {
final var messageSticker = message.getSticker().get();
var sticker = account.getStickerStore().getSticker(messageSticker.getPackId());
final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId());
var sticker = account.getStickerStore().getSticker(stickerPackId);
if (sticker == null) {
sticker = new Sticker(messageSticker.getPackId(), messageSticker.getPackKey());
sticker = new Sticker(stickerPackId, messageSticker.getPackKey());
account.getStickerStore().updateSticker(sticker);
}
}
@ -2086,12 +2088,13 @@ public class Manager implements Closeable {
if (!m.getPackId().isPresent()) {
continue;
}
var sticker = account.getStickerStore().getSticker(m.getPackId().get());
final var stickerPackId = StickerPackId.deserialize(m.getPackId().get());
var sticker = account.getStickerStore().getSticker(stickerPackId);
if (sticker == null) {
if (!m.getPackKey().isPresent()) {
continue;
}
sticker = new Sticker(m.getPackId().get(), m.getPackKey().get());
sticker = new Sticker(stickerPackId, m.getPackKey().get());
}
sticker.setInstalled(!m.getType().isPresent()
|| m.getType().get() == StickerPackOperationMessage.Type.INSTALL);

View file

@ -1,13 +1,7 @@
package org.asamk.signal.manager.storage;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.contacts.ContactsStore;
@ -32,7 +26,6 @@ import org.asamk.signal.manager.storage.stickers.StickerStore;
import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore;
import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.Utils;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger;
@ -69,9 +62,11 @@ public class SignalAccount implements Closeable {
private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
private final ObjectMapper jsonProcessor = new ObjectMapper();
private final ObjectMapper jsonProcessor = Utils.createStorageObjectMapper();
private final FileChannel fileChannel;
private final FileLock lock;
private String username;
private UUID uuid;
private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
@ -94,17 +89,13 @@ public class SignalAccount implements Closeable {
private JsonGroupStore groupStore;
private RecipientStore recipientStore;
private StickerStore stickerStore;
private StickerStore.Storage stickerStoreStorage;
private MessageCache messageCache;
private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
this.fileChannel = fileChannel;
this.lock = lock;
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print
jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
}
public static SignalAccount load(File dataPath, String username) throws IOException {
@ -137,24 +128,10 @@ public class SignalAccount implements Closeable {
account.username = username;
account.profileKey = profileKey;
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username),
account::mergeRecipients);
account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
account.recipientStore::resolveRecipient);
account.identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
account.recipientStore::resolveRecipient,
identityKey,
registrationId);
account.signalProtocolStore = new SignalProtocolStore(account.preKeyStore,
account.signedPreKeyStore,
account.sessionStore,
account.identityKeyStore);
account.stickerStore = new StickerStore();
account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
account.initStores(dataPath, identityKey, registrationId);
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.stickerStore = new StickerStore(account::saveStickerStore);
account.registered = false;
@ -163,6 +140,23 @@ public class SignalAccount implements Closeable {
return account;
}
private void initStores(
final File dataPath, final IdentityKeyPair identityKey, final int registrationId
) throws IOException {
recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients);
preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore::resolveRecipient);
identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
recipientStore::resolveRecipient,
identityKey,
registrationId);
signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, identityKeyStore);
messageCache = new MessageCache(getMessageCachePath(dataPath, username));
}
public static SignalAccount createLinkedAccount(
File dataPath,
String username,
@ -187,29 +181,15 @@ public class SignalAccount implements Closeable {
account.password = password;
account.profileKey = profileKey;
account.deviceId = deviceId;
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username),
account::mergeRecipients);
account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
account.recipientStore::resolveRecipient);
account.identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
account.recipientStore::resolveRecipient,
identityKey,
registrationId);
account.signalProtocolStore = new SignalProtocolStore(account.preKeyStore,
account.signedPreKeyStore,
account.sessionStore,
account.identityKeyStore);
account.stickerStore = new StickerStore();
account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
account.initStores(dataPath, identityKey, registrationId);
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.stickerStore = new StickerStore(account::saveStickerStore);
account.registered = true;
account.isMultiDevice = true;
account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
account.migrateLegacyConfigs();
return account;
@ -347,20 +327,10 @@ public class SignalAccount implements Closeable {
registrationId = legacySignalProtocolStore.getLegacyIdentityKeyStore().getLocalRegistrationId();
}
recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients);
preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore::resolveRecipient);
identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
recipientStore::resolveRecipient,
identityKeyPair,
registrationId);
initStores(dataPath, identityKeyPair, registrationId);
loadLegacyStores(rootNode, legacySignalProtocolStore);
signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, identityKeyStore);
var groupStoreNode = rootNode.get("groupStore");
if (groupStoreNode != null) {
groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
@ -370,15 +340,12 @@ public class SignalAccount implements Closeable {
groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
}
var stickerStoreNode = rootNode.get("stickerStore");
if (stickerStoreNode != null) {
stickerStore = jsonProcessor.convertValue(stickerStoreNode, StickerStore.class);
if (rootNode.hasNonNull("stickerStore")) {
stickerStoreStorage = jsonProcessor.convertValue(rootNode.get("stickerStore"), StickerStore.Storage.class);
stickerStore = StickerStore.fromStorage(stickerStoreStorage, this::saveStickerStore);
} else {
stickerStore = new StickerStore(this::saveStickerStore);
}
if (stickerStore == null) {
stickerStore = new StickerStore();
}
messageCache = new MessageCache(getMessageCachePath(dataPath, username));
loadLegacyThreadStore(rootNode);
}
@ -538,48 +505,50 @@ public class SignalAccount implements Closeable {
}
}
private void saveStickerStore(StickerStore.Storage storage) {
this.stickerStoreStorage = storage;
save();
}
public void save() {
if (fileChannel == null) {
return;
}
var rootNode = jsonProcessor.createObjectNode();
rootNode.put("username", username)
.put("uuid", uuid == null ? null : uuid.toString())
.put("deviceId", deviceId)
.put("isMultiDevice", isMultiDevice)
.put("password", password)
.put("registrationId", identityKeyStore.getLocalRegistrationId())
.put("identityPrivateKey",
Base64.getEncoder()
.encodeToString(identityKeyStore.getIdentityKeyPair().getPrivateKey().serialize()))
.put("identityKey",
Base64.getEncoder()
.encodeToString(identityKeyStore.getIdentityKeyPair().getPublicKey().serialize()))
.put("registrationLockPin", registrationLockPin)
.put("pinMasterKey",
pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
.put("storageKey",
storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
.put("preKeyIdOffset", preKeyIdOffset)
.put("nextSignedPreKeyId", nextSignedPreKeyId)
.put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
.put("registered", registered)
.putPOJO("groupStore", groupStore)
.putPOJO("stickerStore", stickerStore);
try {
try (var output = new ByteArrayOutputStream()) {
// Write to memory first to prevent corrupting the file in case of serialization errors
jsonProcessor.writeValue(output, rootNode);
var input = new ByteArrayInputStream(output.toByteArray());
synchronized (fileChannel) {
synchronized (fileChannel) {
var rootNode = jsonProcessor.createObjectNode();
rootNode.put("username", username)
.put("uuid", uuid == null ? null : uuid.toString())
.put("deviceId", deviceId)
.put("isMultiDevice", isMultiDevice)
.put("password", password)
.put("registrationId", identityKeyStore.getLocalRegistrationId())
.put("identityPrivateKey",
Base64.getEncoder()
.encodeToString(identityKeyStore.getIdentityKeyPair().getPrivateKey().serialize()))
.put("identityKey",
Base64.getEncoder()
.encodeToString(identityKeyStore.getIdentityKeyPair().getPublicKey().serialize()))
.put("registrationLockPin", registrationLockPin)
.put("pinMasterKey",
pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
.put("storageKey",
storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
.put("preKeyIdOffset", preKeyIdOffset)
.put("nextSignedPreKeyId", nextSignedPreKeyId)
.put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
.put("registered", registered)
.putPOJO("groupStore", groupStore)
.putPOJO("stickerStore", stickerStoreStorage);
try {
try (var output = new ByteArrayOutputStream()) {
// Write to memory first to prevent corrupting the file in case of serialization errors
jsonProcessor.writeValue(output, rootNode);
var input = new ByteArrayInputStream(output.toByteArray());
fileChannel.position(0);
input.transferTo(Channels.newOutputStream(fileChannel));
fileChannel.truncate(fileChannel.position());
fileChannel.force(false);
}
} catch (Exception e) {
logger.error("Error saving file: {}", e.getMessage());
}
} catch (Exception e) {
logger.error("Error saving file: {}", e.getMessage());
}
}

View file

@ -5,9 +5,12 @@ import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.io.InvalidObjectException;
public class Utils {
private Utils() {
@ -24,4 +27,14 @@ public class Utils {
return jsonProcessor;
}
public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
var node = parent.get(name);
if (node == null || node.isNull()) {
throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ",
name));
}
return node;
}
}

View file

@ -1,5 +1,6 @@
package org.asamk.signal.manager.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
@ -39,6 +40,7 @@ public class JsonGroupStore {
private final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class);
private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonIgnore
public File groupCachePath;
@JsonProperty("groups")
@ -137,6 +139,7 @@ public class JsonGroupStore {
return null;
}
@JsonIgnore
public List<GroupInfo> getGroups() {
final var groups = this.groups.values();
for (var group : groups) {

View file

@ -2,22 +2,22 @@ package org.asamk.signal.manager.storage.stickers;
public class Sticker {
private final byte[] packId;
private final StickerPackId packId;
private final byte[] packKey;
private boolean installed;
public Sticker(final byte[] packId, final byte[] packKey) {
public Sticker(final StickerPackId packId, final byte[] packKey) {
this.packId = packId;
this.packKey = packKey;
}
public Sticker(final byte[] packId, final byte[] packKey, final boolean installed) {
public Sticker(final StickerPackId packId, final byte[] packKey, final boolean installed) {
this.packId = packId;
this.packKey = packKey;
this.installed = installed;
}
public byte[] getPackId() {
public StickerPackId getPackId() {
return packId;
}

View file

@ -0,0 +1,35 @@
package org.asamk.signal.manager.storage.stickers;
import java.util.Arrays;
public class StickerPackId {
private final byte[] id;
private StickerPackId(final byte[] id) {
this.id = id;
}
public static StickerPackId deserialize(byte[] packId) {
return new StickerPackId(packId);
}
public byte[] serialize() {
return id;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final StickerPackId that = (StickerPackId) o;
return Arrays.equals(id, that.id);
}
@Override
public int hashCode() {
return Arrays.hashCode(id);
}
}

View file

@ -1,69 +1,102 @@
package org.asamk.signal.manager.storage.stickers;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class StickerStore {
@JsonSerialize(using = StickersSerializer.class)
@JsonDeserialize(using = StickersDeserializer.class)
private final Map<byte[], Sticker> stickers = new HashMap<>();
private final Map<StickerPackId, Sticker> stickers;
public Sticker getSticker(byte[] packId) {
return stickers.get(packId);
private final Saver saver;
public StickerStore(final Saver saver) {
this.saver = saver;
stickers = new HashMap<>();
}
public StickerStore(final Map<StickerPackId, Sticker> stickers, final Saver saver) {
this.stickers = stickers;
this.saver = saver;
}
public static StickerStore fromStorage(Storage storage, Saver saver) {
final var packIds = new HashSet<StickerPackId>();
final var stickers = storage.stickers.stream().map(s -> {
var packId = StickerPackId.deserialize(Base64.getDecoder().decode(s.packId));
if (packIds.contains(packId)) {
// Remove legacy duplicate packIds ...
return null;
}
packIds.add(packId);
var packKey = Base64.getDecoder().decode(s.packKey);
var installed = s.installed;
return new Sticker(packId, packKey, installed);
}).filter(Objects::nonNull).collect(Collectors.toMap(Sticker::getPackId, s -> s));
return new StickerStore(stickers, saver);
}
public Sticker getSticker(StickerPackId packId) {
synchronized (stickers) {
return stickers.get(packId);
}
}
public void updateSticker(Sticker sticker) {
stickers.put(sticker.getPackId(), sticker);
Storage storage;
synchronized (stickers) {
stickers.put(sticker.getPackId(), sticker);
storage = toStorageLocked();
}
saver.save(storage);
}
private static class StickersSerializer extends JsonSerializer<Map<byte[], Sticker>> {
private Storage toStorageLocked() {
return new Storage(stickers.values()
.stream()
.map(s -> new Storage.Sticker(Base64.getEncoder().encodeToString(s.getPackId().serialize()),
Base64.getEncoder().encodeToString(s.getPackKey()),
s.isInstalled()))
.collect(Collectors.toList()));
}
@Override
public void serialize(
final Map<byte[], Sticker> value, final JsonGenerator jgen, final SerializerProvider provider
) throws IOException {
final var stickers = value.values();
jgen.writeStartArray(stickers.size());
for (var sticker : stickers) {
jgen.writeStartObject();
jgen.writeStringField("packId", Base64.getEncoder().encodeToString(sticker.getPackId()));
jgen.writeStringField("packKey", Base64.getEncoder().encodeToString(sticker.getPackKey()));
jgen.writeBooleanField("installed", sticker.isInstalled());
jgen.writeEndObject();
public static class Storage {
public List<Storage.Sticker> stickers;
// For deserialization
private Storage() {
}
public Storage(final List<Sticker> stickers) {
this.stickers = stickers;
}
private static class Sticker {
public String packId;
public String packKey;
public boolean installed;
// For deserialization
private Sticker() {
}
public Sticker(final String packId, final String packKey, final boolean installed) {
this.packId = packId;
this.packKey = packKey;
this.installed = installed;
}
jgen.writeEndArray();
}
}
private static class StickersDeserializer extends JsonDeserializer<Map<byte[], Sticker>> {
public interface Saver {
@Override
public Map<byte[], Sticker> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
var stickers = new HashMap<byte[], Sticker>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (var n : node) {
var packId = Base64.getDecoder().decode(n.get("packId").asText());
var packKey = Base64.getDecoder().decode(n.get("packKey").asText());
var installed = n.get("installed").asBoolean(false);
stickers.put(packId, new Sticker(packId, packKey, installed));
}
return stickers;
}
void save(Storage storage);
}
}

View file

@ -1,7 +1,5 @@
package org.asamk.signal.manager.util;
import com.fasterxml.jackson.databind.JsonNode;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -13,7 +11,6 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidObjectException;
import java.net.URLConnection;
import java.nio.file.Files;
@ -80,14 +77,4 @@ public class Utils {
return new SignalServiceAddress(null, identifier);
}
}
public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
var node = parent.get(name);
if (node == null || node.isNull()) {
throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ",
name));
}
return node;
}
}