From: Signal Stickers Date: Sun, 29 Dec 2019 21:23:51 +0000 (-0500) Subject: Add support for uploading stickers. X-Git-Tag: v0.6.6~24 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/23845eab4740c293617179e0f5e278f56c950fe4 Add support for uploading stickers. Closes #256 --- diff --git a/src/main/java/org/asamk/signal/JsonStickerPack.java b/src/main/java/org/asamk/signal/JsonStickerPack.java new file mode 100644 index 00000000..4594c5d1 --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonStickerPack.java @@ -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 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 index 00000000..5fea30fe --- /dev/null +++ b/src/main/java/org/asamk/signal/StickerPackInvalidException.java @@ -0,0 +1,8 @@ +package org.asamk.signal; + +public class StickerPackInvalidException extends Exception { + + public StickerPackInvalidException(String message) { + super(message); + } +} diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 24a03e3f..183b40a0 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -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 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 index 00000000..d4c9e155 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java @@ -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; + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index f5bbe146..622eb815 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -16,10 +16,14 @@ */ 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 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.absent()); + + System.out.println("Starting upload process..."); + Pair 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 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)); diff --git a/src/main/java/org/asamk/signal/util/IOUtils.java b/src/main/java/org/asamk/signal/util/IOUtils.java index 434669de..f21c1572 100644 --- a/src/main/java/org/asamk/signal/util/IOUtils.java +++ b/src/main/java/org/asamk/signal/util/IOUtils.java @@ -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()) {