From: AsamK Date: Sun, 13 Jun 2021 11:37:25 +0000 (+0200) Subject: Implement sticker pack retrieval X-Git-Tag: v0.8.4~1 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/2d068997c50e8e75e378e9f0202aa14de3a140a8?ds=sidebyside Implement sticker pack retrieval Fixes #410 --- diff --git a/lib/src/main/java/org/asamk/signal/manager/JsonStickerPack.java b/lib/src/main/java/org/asamk/signal/manager/JsonStickerPack.java index 3ff40585..74ee6d02 100644 --- a/lib/src/main/java/org/asamk/signal/manager/JsonStickerPack.java +++ b/lib/src/main/java/org/asamk/signal/manager/JsonStickerPack.java @@ -18,6 +18,19 @@ public class JsonStickerPack { @JsonProperty public List stickers; + // For deserialization + private JsonStickerPack() { + } + + public JsonStickerPack( + final String title, final String author, final JsonSticker cover, final List stickers + ) { + this.title = title; + this.author = author; + this.cover = cover; + this.stickers = stickers; + } + public static class JsonSticker { @JsonProperty @@ -28,5 +41,15 @@ public class JsonStickerPack { @JsonProperty 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; + } } } diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 2500371f..37b4aa86 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -34,6 +34,9 @@ import org.asamk.signal.manager.helper.GroupV2Helper; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; 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.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfoV1; @@ -202,6 +205,7 @@ public class Manager implements Closeable { private final PinHelper pinHelper; private final AvatarStore avatarStore; private final AttachmentStore attachmentStore; + private final StickerPackStore stickerPackStore; private final SignalSessionLock sessionLock = new SignalSessionLock() { private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); @@ -275,6 +279,7 @@ public class Manager implements Closeable { this::resolveSignalServiceAddress); this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); + this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); } public String getUsername() { @@ -1434,18 +1439,20 @@ public class Manager implements Closeable { var messageSender = createMessageSender(); 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); try { return new URI("https", "signal.art", "/addstickers/", - "pack_id=" + URLEncoder.encode(packId, StandardCharsets.UTF_8) + "&pack_key=" + URLEncoder.encode( - Hex.toStringCondensed(packKey), - StandardCharsets.UTF_8)).toString(); + "pack_id=" + + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8) + + "&pack_key=" + + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)).toString(); } catch (URISyntaxException e) { throw new AssertionError(e); } @@ -1939,6 +1946,7 @@ public class Manager implements Closeable { sticker = new Sticker(stickerPackId, messageSticker.getPackKey()); account.getStickerStore().updateSticker(sticker); } + enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey())); } return actions; } @@ -2461,16 +2469,23 @@ public class Manager implements Closeable { continue; } 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); - if (sticker == null) { - if (!m.getPackKey().isPresent()) { - continue; + if (m.getPackKey().isPresent()) { + if (sticker == null) { + 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); - account.getStickerStore().updateSticker(sticker); + + if (sticker != null) { + sticker.setInstalled(installed); + account.getStickerStore().updateSticker(sticker); + } } } if (syncMessage.getFetchType().isPresent()) { @@ -2939,6 +2954,11 @@ public class Manager implements Closeable { return account.getRecipientStore().resolveRecipientTrusted(address); } + private void enqueueJob(Job job) { + var context = new Context(account, accountManager, messageReceiver, stickerPackStore); + job.run(context); + } + @Override public void close() throws IOException { close(true); diff --git a/lib/src/main/java/org/asamk/signal/manager/PathConfig.java b/lib/src/main/java/org/asamk/signal/manager/PathConfig.java index d96034df..2c851080 100644 --- a/lib/src/main/java/org/asamk/signal/manager/PathConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/PathConfig.java @@ -7,17 +7,22 @@ public class PathConfig { private final File dataPath; private final File attachmentsPath; private final File avatarsPath; + private final File stickerPacksPath; public static PathConfig createDefault(final File settingsPath) { return new PathConfig(new File(settingsPath, "data"), 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.attachmentsPath = attachmentsPath; this.avatarsPath = avatarsPath; + this.stickerPacksPath = stickerPacksPath; } public File getDataPath() { @@ -31,4 +36,8 @@ public class PathConfig { public File getAvatarsPath() { return avatarsPath; } + + public File getStickerPacksPath() { + return stickerPacksPath; + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/StickerPackStore.java b/lib/src/main/java/org/asamk/signal/manager/StickerPackStore.java new file mode 100644 index 00000000..51ebece3 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/StickerPackStore.java @@ -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; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java new file mode 100644 index 00000000..d34669a4 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java @@ -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; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/Job.java b/lib/src/main/java/org/asamk/signal/manager/jobs/Job.java new file mode 100644 index 00000000..142703ee --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/jobs/Job.java @@ -0,0 +1,6 @@ +package org.asamk.signal.manager.jobs; + +public interface Job { + + void run(Context context); +} diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/RetrieveStickerPackJob.java b/lib/src/main/java/org/asamk/signal/manager/jobs/RetrieveStickerPackJob.java new file mode 100644 index 00000000..20042451 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/jobs/RetrieveStickerPackJob.java @@ -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(); + 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()); + } + } +}