]> nmode's Git Repositories - signal-cli/commitdiff
Add support for uploading stickers.
authorSignal Stickers <blueskidoo@protonmail.com>
Sun, 29 Dec 2019 21:23:51 +0000 (16:23 -0500)
committerAsamK <asamk@gmx.de>
Mon, 23 Mar 2020 13:49:17 +0000 (14:49 +0100)
Closes #256

src/main/java/org/asamk/signal/JsonStickerPack.java [new file with mode: 0644]
src/main/java/org/asamk/signal/StickerPackInvalidException.java [new file with mode: 0644]
src/main/java/org/asamk/signal/commands/Commands.java
src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java [new file with mode: 0644]
src/main/java/org/asamk/signal/manager/Manager.java
src/main/java/org/asamk/signal/util/IOUtils.java

diff --git a/src/main/java/org/asamk/signal/JsonStickerPack.java b/src/main/java/org/asamk/signal/JsonStickerPack.java
new file mode 100644 (file)
index 0000000..4594c5d
--- /dev/null
@@ -0,0 +1,29 @@
+package org.asamk.signal;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+public class JsonStickerPack {
+
+    @JsonProperty
+    public String title;
+
+    @JsonProperty
+    public String author;
+
+    @JsonProperty
+    public JsonSticker cover;
+
+    @JsonProperty
+    public List<JsonSticker> stickers;
+
+    public static class JsonSticker {
+
+        @JsonProperty
+        public String emoji;
+
+        @JsonProperty
+        public String file;
+    }
+}
diff --git a/src/main/java/org/asamk/signal/StickerPackInvalidException.java b/src/main/java/org/asamk/signal/StickerPackInvalidException.java
new file mode 100644 (file)
index 0000000..5fea30f
--- /dev/null
@@ -0,0 +1,8 @@
+package org.asamk.signal;
+
+public class StickerPackInvalidException extends Exception {
+
+    public StickerPackInvalidException(String message) {
+        super(message);
+    }
+}
index 24a03e3fa4fedef88989417755109d0f5a74450c..183b40a00a16a07d23e852f76fd77dc25c7a3159 100644 (file)
@@ -33,6 +33,7 @@ public class Commands {
         addCommand("updateGroup", new UpdateGroupCommand());
         addCommand("updateProfile", new UpdateProfileCommand());
         addCommand("verify", new VerifyCommand());
+        addCommand("uploadStickerPack", new UploadStickerPackCommand());
     }
 
     public static Map<String, Command> getCommands() {
diff --git a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java
new file mode 100644 (file)
index 0000000..d4c9e15
--- /dev/null
@@ -0,0 +1,36 @@
+package org.asamk.signal.commands;
+
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+import org.asamk.signal.StickerPackInvalidException;
+import org.asamk.signal.manager.Manager;
+
+import java.io.IOException;
+
+public class UploadStickerPackCommand implements LocalCommand {
+
+    @Override
+    public void attachToSubparser(final Subparser subparser) {
+        subparser.addArgument("path")
+                .help("The path of the manifest.json or a zip file containing the sticker pack you wish to upload.");
+    }
+
+    @Override
+    public int handleCommand(final Namespace ns, final Manager m) {
+        try {
+            String path = ns.getString("path");
+            String url = m.uploadStickerPack(path);
+            System.out.println("");
+            System.out.println("Upload complete! Sticker pack URL:");
+            System.out.println(url);
+            return 0;
+        } catch (IOException e) {
+            System.err.println("Upload error: " + e.getMessage());
+            return 3;
+        } catch (StickerPackInvalidException e) {
+            System.err.println("Invalid sticker pack: " + e.getMessage());
+            return 3;
+        }
+    }
+}
index f5bbe146fddacfbaf65b00514176660bbd840a1f..622eb815a06f42ed066155b1e33b67927aeaf792 100644 (file)
  */
 package org.asamk.signal.manager;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+
 import org.asamk.Signal;
 import org.asamk.signal.AttachmentInvalidException;
 import org.asamk.signal.GroupNotFoundException;
+import org.asamk.signal.JsonStickerPack;
 import org.asamk.signal.NotAGroupMemberException;
+import org.asamk.signal.StickerPackInvalidException;
 import org.asamk.signal.TrustLevel;
 import org.asamk.signal.UserAlreadyExists;
 import org.asamk.signal.storage.SignalAccount;
@@ -77,6 +81,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent;
 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
 import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
 import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
+import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
+import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest.StickerInfo;
 import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
@@ -102,9 +108,13 @@ import org.whispersystems.signalservice.api.util.SleepTimer;
 import org.whispersystems.signalservice.api.util.StreamDetails;
 import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
 import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
+import org.whispersystems.signalservice.internal.push.StickerUploadAttributes;
+import org.whispersystems.signalservice.internal.push.StickerUploadAttributesResponse;
 import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
+import org.whispersystems.signalservice.internal.util.Hex;
 import org.whispersystems.util.Base64;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -121,6 +131,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
@@ -130,6 +141,8 @@ import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
 
 public class Manager implements Signal {
 
@@ -857,6 +870,138 @@ public class Manager implements Signal {
         account.getThreadStore().updateThread(thread);
     }
 
+    public String uploadStickerPack(String path) throws IOException, StickerPackInvalidException {
+        JsonStickerPack pack = parseStickerPack(path);
+
+        if (pack.stickers == null) {
+            throw new StickerPackInvalidException("Must set a 'stickers' field.");
+        }
+
+        if (pack.stickers.isEmpty()) {
+            throw new StickerPackInvalidException("Must include stickers.");
+        }
+
+        List<StickerInfo> stickers = new ArrayList<>(pack.stickers.size());
+        for (int i = 0; i < pack.stickers.size(); i++) {
+            if (pack.stickers.get(i).file == null) {
+                throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
+            }
+            if (!stickerDataContainsPath(path, pack.stickers.get(i).file)) {
+                throw new StickerPackInvalidException("Could not find find " + pack.stickers.get(i).file);
+            }
+
+            StickerInfo stickerInfo = new StickerInfo(i, Optional.fromNullable(pack.stickers.get(i).emoji).or(""));
+            stickers.add(stickerInfo);
+        }
+
+        boolean uniqueCover = false;
+        StickerInfo cover = stickers.get(0);
+        if (pack.cover != null) {
+            if (pack.cover.file == null) {
+                throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
+            }
+            if (!stickerDataContainsPath(path, pack.cover.file)) {
+                throw new StickerPackInvalidException("Could not find find cover " + pack.cover.file);
+            }
+
+            uniqueCover = true;
+            cover = new StickerInfo(pack.stickers.size(), Optional.fromNullable(pack.cover.emoji).or(""));
+        }
+
+        SignalServiceStickerManifest manifest = new SignalServiceStickerManifest(
+                Optional.fromNullable(pack.title).or(""),
+                Optional.fromNullable(pack.author).or(""),
+                cover,
+                stickers);
+
+        SignalServiceMessageSender messageSender = new SignalServiceMessageSender(
+                BaseConfig.serviceConfiguration,
+                null,
+                username,
+                account.getPassword(),
+                account.getDeviceId(),
+                account.getSignalProtocolStore(),
+                BaseConfig.USER_AGENT,
+                account.isMultiDevice(),
+                Optional.fromNullable(messagePipe),
+                Optional.fromNullable(unidentifiedMessagePipe),
+                Optional.<SignalServiceMessageSender.EventListener>absent());
+
+        System.out.println("Starting upload process...");
+        Pair<byte[], StickerUploadAttributesResponse> responsePair = messageSender.getStickerUploadAttributes(stickers.size() + (uniqueCover ? 1 : 0));
+        byte[] packKey = responsePair.first();
+        StickerUploadAttributesResponse response = responsePair.second();
+
+        System.out.println("Uploading manifest...");
+        messageSender.uploadStickerManifest(manifest, packKey, response.getManifest());
+
+        Map<Integer, StickerUploadAttributes> attrById = new HashMap<>();
+
+        for (StickerUploadAttributes attr : response.getStickers()) {
+            attrById.put(attr.getId(), attr);
+        }
+
+        for (int i = 0; i < pack.stickers.size(); i++) {
+            System.out.println("Uploading sticker " + (i+1) + "/" + pack.stickers.size() + "...");
+            StickerUploadAttributes attr = attrById.get(i);
+            if (attr == null) {
+                throw new StickerPackInvalidException("Upload attributes missing for id " + i);
+            }
+
+            byte[] data = readStickerDataFromPath(path, pack.stickers.get(i).file);
+            messageSender.uploadSticker(new ByteArrayInputStream(data), data.length, packKey, attr);
+        }
+
+        if (uniqueCover) {
+            System.out.println("Uploading unique cover...");
+            StickerUploadAttributes attr = attrById.get(pack.stickers.size());
+            if (attr == null) {
+                throw new StickerPackInvalidException("Upload attributes missing for cover with id " + pack.stickers.size());
+            }
+
+            byte[] data = readStickerDataFromPath(path, pack.cover.file);
+            messageSender.uploadSticker(new ByteArrayInputStream(data), data.length, packKey, attr);
+        }
+
+        return "https://signal.art/addstickers/#pack_id=" + response.getPackId() + "&pack_key=" + Hex.toStringCondensed(packKey).replaceAll(" ", "");
+    }
+
+    private static byte[] readStickerDataFromPath(String rootPath, String subFile) throws IOException, StickerPackInvalidException {
+        if (rootPath.endsWith(".zip")) {
+            ZipFile zip = new ZipFile(rootPath);
+            ZipEntry entry = zip.getEntry(subFile);
+            return IOUtils.readFully(zip.getInputStream(entry));
+        } else if (rootPath.endsWith(".json")) {
+            String dir = new File(rootPath).getParent();
+            FileInputStream fis = new FileInputStream(new File(dir, subFile));
+            return IOUtils.readFully(fis);
+        } else {
+            throw new StickerPackInvalidException("Must point to either a ZIP or JSON file.");
+        }
+    }
+
+    private static boolean stickerDataContainsPath(String rootPath, String subFile) throws IOException {
+        if (rootPath.endsWith(".zip")) {
+            ZipFile zip = new ZipFile(rootPath);
+            return zip.getEntry(subFile) != null;
+        } else if (rootPath.endsWith(".json")) {
+            String dir = new File(rootPath).getParent();
+            return new File(dir, subFile).exists();
+        } else {
+            return false;
+        }
+    }
+
+    private static JsonStickerPack parseStickerPack(String rootPath) throws IOException, StickerPackInvalidException {
+        if (!stickerDataContainsPath(rootPath, "manifest.json")) {
+            throw new StickerPackInvalidException("Could not find manifest.json");
+        }
+
+        String json = new String(readStickerDataFromPath(rootPath, "manifest.json"));
+
+        return new ObjectMapper().readValue(json, JsonStickerPack.class);
+    }
+
     private void requestSyncGroups() throws IOException {
         SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
         SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
index 434669de975fa0d463e9ac69a261dced80742e47..f21c1572b57334735b483cba7d827c377483d8d1 100644 (file)
@@ -1,5 +1,8 @@
 package org.asamk.signal.util;
 
+import org.whispersystems.signalservice.internal.util.Util;
+
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
@@ -35,6 +38,12 @@ public class IOUtils {
         return output.toString();
     }
 
+    public static byte[] readFully(InputStream in) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        Util.copy(in, baos);
+        return baos.toByteArray();
+    }
+
     public static void createPrivateDirectories(String directoryPath) throws IOException {
         final File file = new File(directoryPath);
         if (file.exists()) {