Implement sticker pack retrieval

Fixes #410
This commit is contained in:
AsamK 2021-06-13 13:37:25 +02:00
parent f40c351662
commit 2d068997c5
7 changed files with 253 additions and 14 deletions

View file

@ -18,6 +18,19 @@ public class JsonStickerPack {
@JsonProperty @JsonProperty
public List<JsonSticker> stickers; public List<JsonSticker> stickers;
// For deserialization
private JsonStickerPack() {
}
public JsonStickerPack(
final String title, final String author, final JsonSticker cover, final List<JsonSticker> stickers
) {
this.title = title;
this.author = author;
this.cover = cover;
this.stickers = stickers;
}
public static class JsonSticker { public static class JsonSticker {
@JsonProperty @JsonProperty
@ -28,5 +41,15 @@ public class JsonStickerPack {
@JsonProperty @JsonProperty
public String contentType; public String contentType;
// For deserialization
private JsonSticker() {
}
public JsonSticker(final String emoji, final String file, final String contentType) {
this.emoji = emoji;
this.file = file;
this.contentType = contentType;
}
} }
} }

View file

@ -34,6 +34,9 @@ import org.asamk.signal.manager.helper.GroupV2Helper;
import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.ProfileHelper;
import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
import org.asamk.signal.manager.jobs.Context;
import org.asamk.signal.manager.jobs.Job;
import org.asamk.signal.manager.jobs.RetrieveStickerPackJob;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.groups.GroupInfoV1;
@ -202,6 +205,7 @@ public class Manager implements Closeable {
private final PinHelper pinHelper; private final PinHelper pinHelper;
private final AvatarStore avatarStore; private final AvatarStore avatarStore;
private final AttachmentStore attachmentStore; private final AttachmentStore attachmentStore;
private final StickerPackStore stickerPackStore;
private final SignalSessionLock sessionLock = new SignalSessionLock() { private final SignalSessionLock sessionLock = new SignalSessionLock() {
private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
@ -275,6 +279,7 @@ public class Manager implements Closeable {
this::resolveSignalServiceAddress); this::resolveSignalServiceAddress);
this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath());
} }
public String getUsername() { public String getUsername() {
@ -1434,18 +1439,20 @@ public class Manager implements Closeable {
var messageSender = createMessageSender(); var messageSender = createMessageSender();
var packKey = KeyUtils.createStickerUploadKey(); var packKey = KeyUtils.createStickerUploadKey();
var packId = messageSender.uploadStickerManifest(manifest, packKey); var packIdString = messageSender.uploadStickerManifest(manifest, packKey);
var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString));
var sticker = new Sticker(StickerPackId.deserialize(Hex.fromStringCondensed(packId)), packKey); var sticker = new Sticker(packId, packKey);
account.getStickerStore().updateSticker(sticker); account.getStickerStore().updateSticker(sticker);
try { try {
return new URI("https", return new URI("https",
"signal.art", "signal.art",
"/addstickers/", "/addstickers/",
"pack_id=" + URLEncoder.encode(packId, StandardCharsets.UTF_8) + "&pack_key=" + URLEncoder.encode( "pack_id="
Hex.toStringCondensed(packKey), + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8)
StandardCharsets.UTF_8)).toString(); + "&pack_key="
+ URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)).toString();
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@ -1939,6 +1946,7 @@ public class Manager implements Closeable {
sticker = new Sticker(stickerPackId, messageSticker.getPackKey()); sticker = new Sticker(stickerPackId, messageSticker.getPackKey());
account.getStickerStore().updateSticker(sticker); account.getStickerStore().updateSticker(sticker);
} }
enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey()));
} }
return actions; return actions;
} }
@ -2461,16 +2469,23 @@ public class Manager implements Closeable {
continue; continue;
} }
final var stickerPackId = StickerPackId.deserialize(m.getPackId().get()); final var stickerPackId = StickerPackId.deserialize(m.getPackId().get());
final var installed = !m.getType().isPresent()
|| m.getType().get() == StickerPackOperationMessage.Type.INSTALL;
var sticker = account.getStickerStore().getSticker(stickerPackId); var sticker = account.getStickerStore().getSticker(stickerPackId);
if (sticker == null) { if (m.getPackKey().isPresent()) {
if (!m.getPackKey().isPresent()) { if (sticker == null) {
continue; sticker = new Sticker(stickerPackId, m.getPackKey().get());
}
if (installed) {
enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get()));
} }
sticker = new Sticker(stickerPackId, m.getPackKey().get());
} }
sticker.setInstalled(!m.getType().isPresent()
|| m.getType().get() == StickerPackOperationMessage.Type.INSTALL); if (sticker != null) {
account.getStickerStore().updateSticker(sticker); sticker.setInstalled(installed);
account.getStickerStore().updateSticker(sticker);
}
} }
} }
if (syncMessage.getFetchType().isPresent()) { if (syncMessage.getFetchType().isPresent()) {
@ -2939,6 +2954,11 @@ public class Manager implements Closeable {
return account.getRecipientStore().resolveRecipientTrusted(address); return account.getRecipientStore().resolveRecipientTrusted(address);
} }
private void enqueueJob(Job job) {
var context = new Context(account, accountManager, messageReceiver, stickerPackStore);
job.run(context);
}
@Override @Override
public void close() throws IOException { public void close() throws IOException {
close(true); close(true);

View file

@ -7,17 +7,22 @@ public class PathConfig {
private final File dataPath; private final File dataPath;
private final File attachmentsPath; private final File attachmentsPath;
private final File avatarsPath; private final File avatarsPath;
private final File stickerPacksPath;
public static PathConfig createDefault(final File settingsPath) { public static PathConfig createDefault(final File settingsPath) {
return new PathConfig(new File(settingsPath, "data"), return new PathConfig(new File(settingsPath, "data"),
new File(settingsPath, "attachments"), new File(settingsPath, "attachments"),
new File(settingsPath, "avatars")); new File(settingsPath, "avatars"),
new File(settingsPath, "stickers"));
} }
private PathConfig(final File dataPath, final File attachmentsPath, final File avatarsPath) { private PathConfig(
final File dataPath, final File attachmentsPath, final File avatarsPath, final File stickerPacksPath
) {
this.dataPath = dataPath; this.dataPath = dataPath;
this.attachmentsPath = attachmentsPath; this.attachmentsPath = attachmentsPath;
this.avatarsPath = avatarsPath; this.avatarsPath = avatarsPath;
this.stickerPacksPath = stickerPacksPath;
} }
public File getDataPath() { public File getDataPath() {
@ -31,4 +36,8 @@ public class PathConfig {
public File getAvatarsPath() { public File getAvatarsPath() {
return avatarsPath; return avatarsPath;
} }
public File getStickerPacksPath() {
return stickerPacksPath;
}
} }

View file

@ -0,0 +1,65 @@
package org.asamk.signal.manager;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.asamk.signal.manager.storage.stickers.StickerPackId;
import org.asamk.signal.manager.util.IOUtils;
import org.whispersystems.signalservice.internal.util.Hex;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
public class StickerPackStore {
private final File stickersPath;
public StickerPackStore(final File stickersPath) {
this.stickersPath = stickersPath;
}
public boolean existsStickerPack(StickerPackId stickerPackId) {
return getStickerPackManifestFile(stickerPackId).exists();
}
public void storeManifest(StickerPackId stickerPackId, JsonStickerPack manifest) throws IOException {
try (OutputStream output = new FileOutputStream(getStickerPackManifestFile(stickerPackId))) {
try (var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) {
new ObjectMapper().writeValue(writer, manifest);
}
}
}
public void storeSticker(StickerPackId stickerPackId, int stickerId, StickerStorer storer) throws IOException {
createStickerPackDir(stickerPackId);
try (OutputStream output = new FileOutputStream(getStickerPackStickerFile(stickerPackId, stickerId))) {
storer.store(output);
}
}
private File getStickerPackManifestFile(StickerPackId stickerPackId) {
return new File(getStickerPackPath(stickerPackId), "manifest.json");
}
private File getStickerPackStickerFile(StickerPackId stickerPackId, int stickerId) {
return new File(getStickerPackPath(stickerPackId), String.valueOf(stickerId));
}
private File getStickerPackPath(StickerPackId stickerPackId) {
return new File(stickersPath, Hex.toStringCondensed(stickerPackId.serialize()));
}
private void createStickerPackDir(StickerPackId stickerPackId) throws IOException {
IOUtils.createPrivateDirectories(getStickerPackPath(stickerPackId));
}
@FunctionalInterface
public interface StickerStorer {
void store(OutputStream outputStream) throws IOException;
}
}

View file

@ -0,0 +1,42 @@
package org.asamk.signal.manager.jobs;
import org.asamk.signal.manager.StickerPackStore;
import org.asamk.signal.manager.storage.SignalAccount;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
public class Context {
private SignalAccount account;
private SignalServiceAccountManager accountManager;
private SignalServiceMessageReceiver messageReceiver;
private StickerPackStore stickerPackStore;
public Context(
final SignalAccount account,
final SignalServiceAccountManager accountManager,
final SignalServiceMessageReceiver messageReceiver,
final StickerPackStore stickerPackStore
) {
this.account = account;
this.accountManager = accountManager;
this.messageReceiver = messageReceiver;
this.stickerPackStore = stickerPackStore;
}
public SignalAccount getAccount() {
return account;
}
public SignalServiceAccountManager getAccountManager() {
return accountManager;
}
public SignalServiceMessageReceiver getMessageReceiver() {
return messageReceiver;
}
public StickerPackStore getStickerPackStore() {
return stickerPackStore;
}
}

View file

@ -0,0 +1,6 @@
package org.asamk.signal.manager.jobs;
public interface Job {
void run(Context context);
}

View file

@ -0,0 +1,74 @@
package org.asamk.signal.manager.jobs;
import org.asamk.signal.manager.JsonStickerPack;
import org.asamk.signal.manager.storage.stickers.StickerPackId;
import org.asamk.signal.manager.util.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.signalservice.internal.util.Hex;
import java.io.IOException;
import java.util.HashSet;
import java.util.stream.Collectors;
public class RetrieveStickerPackJob implements Job {
private final static Logger logger = LoggerFactory.getLogger(RetrieveStickerPackJob.class);
private final StickerPackId packId;
private final byte[] packKey;
public RetrieveStickerPackJob(final StickerPackId packId, final byte[] packKey) {
this.packId = packId;
this.packKey = packKey;
}
@Override
public void run(Context context) {
if (context.getStickerPackStore().existsStickerPack(packId)) {
logger.debug("Sticker pack {} already downloaded.", Hex.toStringCondensed(packId.serialize()));
return;
}
logger.debug("Retrieving sticker pack {}.", Hex.toStringCondensed(packId.serialize()));
try {
final var manifest = context.getMessageReceiver().retrieveStickerManifest(packId.serialize(), packKey);
final var stickerIds = new HashSet<Integer>();
if (manifest.getCover().isPresent()) {
stickerIds.add(manifest.getCover().get().getId());
}
for (var sticker : manifest.getStickers()) {
stickerIds.add(sticker.getId());
}
for (var id : stickerIds) {
final var inputStream = context.getMessageReceiver().retrieveSticker(packId.serialize(), packKey, id);
context.getStickerPackStore().storeSticker(packId, id, o -> IOUtils.copyStream(inputStream, o));
}
final var jsonManifest = new JsonStickerPack(manifest.getTitle().orNull(),
manifest.getAuthor().orNull(),
manifest.getCover()
.transform(c -> new JsonStickerPack.JsonSticker(c.getEmoji(),
String.valueOf(c.getId()),
c.getContentType()))
.orNull(),
manifest.getStickers()
.stream()
.map(c -> new JsonStickerPack.JsonSticker(c.getEmoji(),
String.valueOf(c.getId()),
c.getContentType()))
.collect(Collectors.toList()));
context.getStickerPackStore().storeManifest(packId, jsonManifest);
} catch (IOException e) {
logger.warn("Failed to retrieve sticker pack {}: {}",
Hex.toStringCondensed(packId.serialize()),
e.getMessage());
} catch (InvalidMessageException e) {
logger.warn("Failed to retrieve sticker pack {}, invalid pack data: {}",
Hex.toStringCondensed(packId.serialize()),
e.getMessage());
}
}
}