]> nmode's Git Repositories - signal-cli/commitdiff
Implement sticker pack retrieval
authorAsamK <asamk@gmx.de>
Sun, 13 Jun 2021 11:37:25 +0000 (13:37 +0200)
committerAsamK <asamk@gmx.de>
Sun, 13 Jun 2021 13:02:16 +0000 (15:02 +0200)
Fixes #410

lib/src/main/java/org/asamk/signal/manager/JsonStickerPack.java
lib/src/main/java/org/asamk/signal/manager/Manager.java
lib/src/main/java/org/asamk/signal/manager/PathConfig.java
lib/src/main/java/org/asamk/signal/manager/StickerPackStore.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/jobs/Context.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/jobs/Job.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/jobs/RetrieveStickerPackJob.java [new file with mode: 0644]

index 3ff40585df7ab66f756c56ed9628d0e195156240..74ee6d0231b32865a6cde3c7f3d4570d0b6ff5d6 100644 (file)
@@ -18,6 +18,19 @@ public class JsonStickerPack {
     @JsonProperty
     public List<JsonSticker> stickers;
 
     @JsonProperty
     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 {
 
         @JsonProperty
     public static class JsonSticker {
 
         @JsonProperty
@@ -28,5 +41,15 @@ public class JsonStickerPack {
 
         @JsonProperty
         public String contentType;
 
         @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;
+        }
     }
 }
     }
 }
index 2500371f566a231c342ee396c21543c759d4556e..37b4aa860e6ead15b1562923bc9ca323acf76276 100644 (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.ProfileHelper;
 import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
 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;
 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 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();
 
     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::resolveSignalServiceAddress);
         this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
         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 packKey = KeyUtils.createStickerUploadKey();
         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/",
         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);
         }
         } 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);
             }
                 sticker = new Sticker(stickerPackId, messageSticker.getPackKey());
                 account.getStickerStore().updateSticker(sticker);
             }
+            enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey()));
         }
         return actions;
     }
         }
         return actions;
     }
@@ -2461,16 +2469,23 @@ public class Manager implements Closeable {
                             continue;
                         }
                         final var stickerPackId = StickerPackId.deserialize(m.getPackId().get());
                             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);
                         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()) {
                     }
                 }
                 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
     public void close() throws IOException {
         close(true);
     @Override
     public void close() throws IOException {
         close(true);
index d96034dfd169cdce3cc5057df02b5ee2c405f1dd..2c85108001e5da33db0c65e610a510ea01a8ad93 100644 (file)
@@ -7,17 +7,22 @@ public class PathConfig {
     private final File dataPath;
     private final File attachmentsPath;
     private final File avatarsPath;
     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"),
 
     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.dataPath = dataPath;
         this.attachmentsPath = attachmentsPath;
         this.avatarsPath = avatarsPath;
+        this.stickerPacksPath = stickerPacksPath;
     }
 
     public File getDataPath() {
     }
 
     public File getDataPath() {
@@ -31,4 +36,8 @@ public class PathConfig {
     public File getAvatarsPath() {
         return avatarsPath;
     }
     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 (file)
index 0000000..51ebece
--- /dev/null
@@ -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 (file)
index 0000000..d34669a
--- /dev/null
@@ -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 (file)
index 0000000..142703e
--- /dev/null
@@ -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 (file)
index 0000000..2004245
--- /dev/null
@@ -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());
+        }
+    }
+}