Add support for uploading stickers.

Closes #256
This commit is contained in:
Signal Stickers 2019-12-29 16:23:51 -05:00 committed by AsamK
parent 3f315df6c8
commit 23845eab47
6 changed files with 228 additions and 0 deletions

View file

@ -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;
}
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal;
public class StickerPackInvalidException extends Exception {
public StickerPackInvalidException(String message) {
super(message);
}
}

View file

@ -33,6 +33,7 @@ public class Commands {
addCommand("updateGroup", new UpdateGroupCommand()); addCommand("updateGroup", new UpdateGroupCommand());
addCommand("updateProfile", new UpdateProfileCommand()); addCommand("updateProfile", new UpdateProfileCommand());
addCommand("verify", new VerifyCommand()); addCommand("verify", new VerifyCommand());
addCommand("uploadStickerPack", new UploadStickerPackCommand());
} }
public static Map<String, Command> getCommands() { public static Map<String, Command> getCommands() {

View file

@ -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;
}
}
}

View file

@ -16,10 +16,14 @@
*/ */
package org.asamk.signal.manager; package org.asamk.signal.manager;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.asamk.Signal; import org.asamk.Signal;
import org.asamk.signal.AttachmentInvalidException; import org.asamk.signal.AttachmentInvalidException;
import org.asamk.signal.GroupNotFoundException; import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.JsonStickerPack;
import org.asamk.signal.NotAGroupMemberException; import org.asamk.signal.NotAGroupMemberException;
import org.asamk.signal.StickerPackInvalidException;
import org.asamk.signal.TrustLevel; import org.asamk.signal.TrustLevel;
import org.asamk.signal.UserAlreadyExists; import org.asamk.signal.UserAlreadyExists;
import org.asamk.signal.storage.SignalAccount; 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.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup; 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.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; 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.StreamDetails;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos; 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.push.UnsupportedDataMessageException;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.util.Base64; import org.whispersystems.util.Base64;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@ -121,6 +131,7 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -130,6 +141,8 @@ import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class Manager implements Signal { public class Manager implements Signal {
@ -857,6 +870,138 @@ public class Manager implements Signal {
account.getThreadStore().updateThread(thread); 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 { private void requestSyncGroups() throws IOException {
SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build(); SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));

View file

@ -1,5 +1,8 @@
package org.asamk.signal.util; package org.asamk.signal.util;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -35,6 +38,12 @@ public class IOUtils {
return output.toString(); 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 { public static void createPrivateDirectories(String directoryPath) throws IOException {
final File file = new File(directoryPath); final File file = new File(directoryPath);
if (file.exists()) { if (file.exists()) {