]> nmode's Git Repositories - signal-cli/blobdiff - src/main/java/org/asamk/signal/manager/Manager.java
Set uuid after verify and linking and request it at startup for existing clients
[signal-cli] / src / main / java / org / asamk / signal / manager / Manager.java
index fffa501337bb15e1b97784843cf50d86c8664d23..0aada70364fa2802448136be4e12cfdd40886dae 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;
@@ -27,7 +31,6 @@ import org.asamk.signal.storage.contacts.ContactInfo;
 import org.asamk.signal.storage.groups.GroupInfo;
 import org.asamk.signal.storage.groups.JsonGroupStore;
 import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
-import org.asamk.signal.storage.threads.ThreadInfo;
 import org.asamk.signal.util.IOUtils;
 import org.asamk.signal.util.Util;
 import org.signal.libsignal.metadata.InvalidMetadataMessageException;
@@ -77,6 +80,9 @@ 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.SignalServiceReceiptMessage;
+import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload;
+import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload.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;
@@ -103,6 +109,7 @@ 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.UnsupportedDataMessageException;
+import org.whispersystems.signalservice.internal.util.Hex;
 import org.whispersystems.util.Base64;
 
 import java.io.File;
@@ -113,6 +120,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
@@ -128,8 +137,11 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.UUID;
 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 {
 
@@ -160,12 +172,12 @@ public class Manager implements Signal {
         return username;
     }
 
-    private SignalServiceAddress getSelfAddress() {
-        return new SignalServiceAddress(null, username);
+    public SignalServiceAddress getSelfAddress() {
+        return account.getSelfAddress();
     }
 
     private SignalServiceAccountManager getSignalServiceAccountManager() {
-        return new SignalServiceAccountManager(BaseConfig.serviceConfiguration, null, account.getUsername(), account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer);
+        return new SignalServiceAccountManager(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer);
     }
 
     private IdentityKey getIdentity() {
@@ -204,9 +216,15 @@ public class Manager implements Signal {
 
         accountManager = getSignalServiceAccountManager();
         try {
-            if (account.isRegistered() && accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) {
-                refreshPreKeys();
-                account.save();
+            if (account.isRegistered()) {
+                if (accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) {
+                    refreshPreKeys();
+                    account.save();
+                }
+                if (account.getUuid() == null) {
+                    account.setUuid(accountManager.getOwnUuid());
+                    account.save();
+                }
             }
         } catch (AuthorizationFailedException e) {
             System.err.println("Authorization failed, was the number registered elsewhere?");
@@ -334,7 +352,7 @@ public class Manager implements Signal {
                 throw new IOException("Received invalid profileKey", e);
             }
         }
-        account = SignalAccount.createLinkedAccount(dataPath, username, account.getPassword(), ret.getDeviceId(), ret.getIdentity(), account.getSignalProtocolStore().getLocalRegistrationId(), account.getSignalingKey(), profileKey);
+        account = SignalAccount.createLinkedAccount(dataPath, username, ret.getUuid(), account.getPassword(), ret.getDeviceId(), ret.getIdentity(), account.getSignalProtocolStore().getLocalRegistrationId(), account.getSignalingKey(), profileKey);
 
         refreshPreKeys();
 
@@ -412,10 +430,11 @@ public class Manager implements Signal {
         verificationCode = verificationCode.replace("-", "");
         account.setSignalingKey(KeyUtils.createSignalingKey());
         // TODO make unrestricted unidentified access configurable
-        accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, capabilities);
+        UUID uuid = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, capabilities);
 
         //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
         account.setRegistered(true);
+        account.setUuid(uuid);
         account.setRegistrationLockPin(pin);
 
         refreshPreKeys();
@@ -442,11 +461,11 @@ public class Manager implements Signal {
     }
 
     private SignalServiceMessageReceiver getMessageReceiver() {
-        return new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, null, username, account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer);
+        return new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer);
     }
 
     private SignalServiceMessageSender getMessageSender() {
-        return new SignalServiceMessageSender(BaseConfig.serviceConfiguration, null, username, account.getPassword(),
+        return new SignalServiceMessageSender(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(),
                 account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent());
     }
 
@@ -492,12 +511,10 @@ public class Manager implements Signal {
         if (g == null) {
             throw new GroupNotFoundException(groupId);
         }
-        for (String member : g.members) {
-            if (member.equals(this.username)) {
-                return g;
-            }
+        if (!g.isMember(account.getSelfAddress())) {
+            throw new NotAGroupMemberException(groupId, g.name);
         }
-        throw new NotAGroupMemberException(groupId, g.name);
+        return g;
     }
 
     public List<GroupInfo> getGroups() {
@@ -518,17 +535,12 @@ public class Manager implements Signal {
                     .build();
             messageBuilder.asGroupMessage(group);
         }
-        ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(groupId));
-        if (thread != null) {
-            messageBuilder.withExpiration(thread.messageExpirationTime);
-        }
 
         final GroupInfo g = getGroupForSending(groupId);
 
-        // Don't send group message to ourself
-        final List<String> membersSend = new ArrayList<>(g.members);
-        membersSend.remove(this.username);
-        sendMessageLegacy(messageBuilder, membersSend);
+        messageBuilder.withExpiration(g.messageExpirationTime);
+
+        sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
     }
 
     public void sendGroupMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor,
@@ -545,10 +557,7 @@ public class Manager implements Signal {
             messageBuilder.asGroupMessage(group);
         }
         final GroupInfo g = getGroupForSending(groupId);
-        // Don't send group message to ourself
-        final List<String> membersSend = new ArrayList<>(g.members);
-        membersSend.remove(this.username);
-        sendMessageLegacy(messageBuilder, membersSend);
+        sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
     }
 
     public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
@@ -560,18 +569,18 @@ public class Manager implements Signal {
                 .asGroupMessage(group);
 
         final GroupInfo g = getGroupForSending(groupId);
-        g.members.remove(this.username);
+        g.removeMember(account.getSelfAddress());
         account.getGroupStore().updateGroup(g);
 
-        sendMessageLegacy(messageBuilder, g.members);
+        sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
     }
 
-    private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
+    private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<SignalServiceAddress> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
         GroupInfo g;
         if (groupId == null) {
             // Create new group
             g = new GroupInfo(KeyUtils.createGroupId());
-            g.members.add(username);
+            g.addMembers(Collections.singleton(account.getSelfAddress()));
         } else {
             g = getGroupForSending(groupId);
         }
@@ -581,31 +590,26 @@ public class Manager implements Signal {
         }
 
         if (members != null) {
-            Set<String> newMembers = new HashSet<>();
-            for (String member : members) {
-                try {
-                    member = Utils.canonicalizeNumber(member, username);
-                } catch (InvalidNumberException e) {
-                    System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
-                    System.err.println("Aborting…");
-                    System.exit(1);
-                }
-                if (g.members.contains(member)) {
+            final Set<String> newE164Members = new HashSet<>();
+            for (SignalServiceAddress member : members) {
+                if (g.isMember(member) || !member.getNumber().isPresent()) {
                     continue;
                 }
-                newMembers.add(member);
-                g.members.add(member);
+                newE164Members.add(member.getNumber().get());
             }
-            final List<ContactTokenDetails> contacts = accountManager.getContacts(newMembers);
-            if (contacts.size() != newMembers.size()) {
+
+            final List<ContactTokenDetails> contacts = accountManager.getContacts(newE164Members);
+            if (contacts.size() != newE164Members.size()) {
                 // Some of the new members are not registered on Signal
                 for (ContactTokenDetails contact : contacts) {
-                    newMembers.remove(contact.getNumber());
+                    newE164Members.remove(contact.getNumber());
                 }
-                System.err.println("Failed to add members " + Util.join(", ", newMembers) + " to group: Not registered on Signal");
+                System.err.println("Failed to add members " + Util.join(", ", newE164Members) + " to group: Not registered on Signal");
                 System.err.println("Aborting…");
                 System.exit(1);
             }
+
+            g.addMembers(members);
         }
 
         if (avatarFile != null) {
@@ -618,29 +622,24 @@ public class Manager implements Signal {
 
         SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
 
-        // Don't send group message to ourself
-        final List<String> membersSend = new ArrayList<>(g.members);
-        membersSend.remove(this.username);
-        sendMessageLegacy(messageBuilder, membersSend);
+        sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
         return g.groupId;
     }
 
-    private void sendUpdateGroupMessage(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
+    private void sendUpdateGroupMessage(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions {
         if (groupId == null) {
             return;
         }
         GroupInfo g = getGroupForSending(groupId);
 
-        if (!g.members.contains(recipient)) {
+        if (!g.isMember(recipient)) {
             return;
         }
 
         SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
 
         // Send group message only to the recipient who requested it
-        final List<String> membersSend = new ArrayList<>();
-        membersSend.add(recipient);
-        sendMessageLegacy(messageBuilder, membersSend);
+        sendMessageLegacy(messageBuilder, Collections.singleton(recipient));
     }
 
     private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) {
@@ -658,18 +657,12 @@ public class Manager implements Signal {
             }
         }
 
-        SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
-                .asGroupMessage(group.build());
-
-        ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(g.groupId));
-        if (thread != null) {
-            messageBuilder.withExpiration(thread.messageExpirationTime);
-        }
-
-        return messageBuilder;
+        return SignalServiceDataMessage.newBuilder()
+                .asGroupMessage(group.build())
+                .withExpiration(g.messageExpirationTime);
     }
 
-    private void sendGroupInfoRequest(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
+    private void sendGroupInfoRequest(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions {
         if (groupId == null) {
             return;
         }
@@ -680,20 +673,21 @@ public class Manager implements Signal {
         SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
                 .asGroupMessage(group.build());
 
-        ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(groupId));
-        if (thread != null) {
-            messageBuilder.withExpiration(thread.messageExpirationTime);
-        }
-
         // Send group info request message to the recipient who sent us a message with this groupId
-        final List<String> membersSend = new ArrayList<>();
-        membersSend.add(recipient);
-        sendMessageLegacy(messageBuilder, membersSend);
+        sendMessageLegacy(messageBuilder, Collections.singleton(recipient));
+    }
+
+    private void sendReceipt(SignalServiceAddress remoteAddress, long messageId) throws IOException, UntrustedIdentityException {
+        SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY,
+                Collections.singletonList(messageId),
+                System.currentTimeMillis());
+
+        getMessageSender().sendReceipt(remoteAddress, getAccessFor(remoteAddress), receiptMessage);
     }
 
     @Override
     public void sendMessage(String message, List<String> attachments, String recipient)
-            throws EncapsulatedExceptions, AttachmentInvalidException, IOException {
+            throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException {
         List<String> recipients = new ArrayList<>(1);
         recipients.add(recipient);
         sendMessage(message, attachments, recipients);
@@ -702,7 +696,7 @@ public class Manager implements Signal {
     @Override
     public void sendMessage(String messageText, List<String> attachments,
                             List<String> recipients)
-            throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
+            throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException {
         final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
         if (attachments != null) {
             List<SignalServiceAttachment> attachmentStreams = Utils.getSignalServiceAttachments(attachments);
@@ -721,31 +715,31 @@ public class Manager implements Signal {
             messageBuilder.withAttachments(attachmentPointers);
         }
         messageBuilder.withProfileKey(account.getProfileKey().serialize());
-        sendMessageLegacy(messageBuilder, recipients);
+        sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients));
     }
 
     public void sendMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor,
                                     long targetSentTimestamp, List<String> recipients)
-            throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
+            throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException {
         SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp);
         final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
                 .withReaction(reaction)
                 .withProfileKey(account.getProfileKey().serialize());
-        sendMessageLegacy(messageBuilder, recipients);
+        sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients));
     }
 
     @Override
-    public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions {
+    public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException {
         SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
                 .asEndSessionMessage();
 
-        sendMessageLegacy(messageBuilder, recipients);
+        sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients));
     }
 
     @Override
     public String getContactName(String number) throws InvalidNumberException {
-        String canonicalizedNumber = Utils.canonicalizeNumber(number, username);
-        ContactInfo contact = account.getContactStore().getContact(canonicalizedNumber);
+        String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername());
+        ContactInfo contact = account.getContactStore().getContact(new SignalServiceAddress(null, canonicalizedNumber));
         if (contact == null) {
             return "";
         } else {
@@ -755,11 +749,11 @@ public class Manager implements Signal {
 
     @Override
     public void setContactName(String number, String name) throws InvalidNumberException {
-        String canonicalizedNumber = Utils.canonicalizeNumber(number, username);
-        ContactInfo contact = account.getContactStore().getContact(canonicalizedNumber);
+        String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername());
+        final SignalServiceAddress address = new SignalServiceAddress(null, canonicalizedNumber);
+        ContactInfo contact = account.getContactStore().getContact(address);
         if (contact == null) {
-            contact = new ContactInfo();
-            contact.number = canonicalizedNumber;
+            contact = new ContactInfo(address);
             System.err.println("Add contact " + canonicalizedNumber + " named " + name);
         } else {
             System.err.println("Updating contact " + canonicalizedNumber + " name " + contact.name + " -> " + name);
@@ -771,11 +765,11 @@ public class Manager implements Signal {
 
     @Override
     public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException {
-        number = Utils.canonicalizeNumber(number, username);
-        ContactInfo contact = account.getContactStore().getContact(number);
+        number = Utils.canonicalizeNumber(number, account.getUsername());
+        final SignalServiceAddress address = new SignalServiceAddress(null, number);
+        ContactInfo contact = account.getContactStore().getContact(address);
         if (contact == null) {
-            contact = new ContactInfo();
-            contact.number = number;
+            contact = new ContactInfo(address);
             System.err.println("Adding and " + (blocked ? "blocking" : "unblocking") + " contact " + number);
         } else {
             System.err.println((blocked ? "Blocking" : "Unblocking") + " contact " + number);
@@ -822,14 +816,14 @@ public class Manager implements Signal {
     public List<String> getGroupMembers(byte[] groupId) {
         GroupInfo group = getGroup(groupId);
         if (group == null) {
-            return new ArrayList<>();
+            return Collections.emptyList();
         } else {
-            return new ArrayList<>(group.members);
+            return new ArrayList<>(group.getMembersE164());
         }
     }
 
     @Override
-    public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
+    public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException {
         if (groupId.length == 0) {
             groupId = null;
         }
@@ -842,19 +836,130 @@ public class Manager implements Signal {
         if (avatar.isEmpty()) {
             avatar = null;
         }
-        return sendUpdateGroupMessage(groupId, name, members, avatar);
+        return sendUpdateGroupMessage(groupId, name, members == null ? null : getSignalServiceAddresses(members), avatar);
+    }
+
+    /**
+     * Change the expiration timer for a contact
+     */
+    public void setExpirationTimer(SignalServiceAddress address, int messageExpirationTimer) {
+        ContactInfo c = account.getContactStore().getContact(address);
+        c.messageExpirationTime = messageExpirationTimer;
+        account.getContactStore().updateContact(c);
     }
 
     /**
-     * Change the expiration timer for a thread (number of groupId)
+     * Change the expiration timer for a group
+     */
+    public void setExpirationTimer(byte[] groupId, int messageExpirationTimer) {
+        GroupInfo g = account.getGroupStore().getGroup(groupId);
+        g.messageExpirationTime = messageExpirationTimer;
+        account.getGroupStore().updateGroup(g);
+    }
+
+    /**
+     * Upload the sticker pack from path.
      *
-     * @param numberOrGroupId
-     * @param messageExpirationTimer
+     * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
+     * @return if successful, returns the URL to install the sticker pack in the signal app
      */
-    public void setExpirationTimer(String numberOrGroupId, int messageExpirationTimer) {
-        ThreadInfo thread = account.getThreadStore().getThread(numberOrGroupId);
-        thread.messageExpirationTime = messageExpirationTimer;
-        account.getThreadStore().updateThread(thread);
+    public String uploadStickerPack(String path) throws IOException, StickerPackInvalidException {
+        SignalServiceStickerManifestUpload manifest = getSignalServiceStickerManifestUpload(path);
+
+        SignalServiceMessageSender messageSender = getMessageSender();
+
+        byte[] packKey = KeyUtils.createStickerUploadKey();
+        String packId = messageSender.uploadStickerManifest(manifest, packKey);
+
+        try {
+            return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder.encode(packId, "utf-8") + "&pack_key=" + URLEncoder.encode(Hex.toStringCondensed(packKey), "utf-8"))
+                    .toString();
+        } catch (URISyntaxException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    private SignalServiceStickerManifestUpload getSignalServiceStickerManifestUpload(final String path) throws IOException, StickerPackInvalidException {
+        ZipFile zip = null;
+        String rootPath = null;
+
+        final File file = new File(path);
+        if (file.getName().endsWith(".zip")) {
+            zip = new ZipFile(file);
+        } else if (file.getName().equals("manifest.json")) {
+            rootPath = file.getParent();
+        } else {
+            throw new StickerPackInvalidException("Could not find manifest.json");
+        }
+
+        JsonStickerPack pack = parseStickerPack(rootPath, zip);
+
+        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 (JsonStickerPack.JsonSticker sticker : pack.stickers) {
+            if (sticker.file == null) {
+                throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
+            }
+
+            Pair<InputStream, Long> data;
+            try {
+                data = getInputStreamAndLength(rootPath, zip, sticker.file);
+            } catch (IOException ignored) {
+                throw new StickerPackInvalidException("Could not find find " + sticker.file);
+            }
+
+            StickerInfo stickerInfo = new StickerInfo(data.first(), data.second(), Optional.fromNullable(sticker.emoji).or(""));
+            stickers.add(stickerInfo);
+        }
+
+        StickerInfo cover = null;
+        if (pack.cover != null) {
+            if (pack.cover.file == null) {
+                throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
+            }
+
+            Pair<InputStream, Long> data;
+            try {
+                data = getInputStreamAndLength(rootPath, zip, pack.cover.file);
+            } catch (IOException ignored) {
+                throw new StickerPackInvalidException("Could not find find " + pack.cover.file);
+            }
+
+            cover = new StickerInfo(data.first(), data.second(), Optional.fromNullable(pack.cover.emoji).or(""));
+        }
+
+        return new SignalServiceStickerManifestUpload(
+                pack.title,
+                pack.author,
+                cover,
+                stickers);
+    }
+
+    private static JsonStickerPack parseStickerPack(String rootPath, ZipFile zip) throws IOException {
+        InputStream inputStream;
+        if (zip != null) {
+            inputStream = zip.getInputStream(zip.getEntry("manifest.json"));
+        } else {
+            inputStream = new FileInputStream((new File(rootPath, "manifest.json")));
+        }
+        return new ObjectMapper().readValue(inputStream, JsonStickerPack.class);
+    }
+
+    private static Pair<InputStream, Long> getInputStreamAndLength(final String rootPath, final ZipFile zip, final String subfile) throws IOException {
+        if (zip != null) {
+            final ZipEntry entry = zip.getEntry(subfile);
+            return new Pair<>(zip.getInputStream(entry), entry.getSize());
+        } else {
+            final File file = new File(rootPath, subfile);
+            return new Pair<>(new FileInputStream(file), file.length());
+        }
     }
 
     private void requestSyncGroups() throws IOException {
@@ -923,7 +1028,7 @@ public class Manager implements Signal {
     }
 
     private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) throws IOException {
-        ContactInfo contact = account.getContactStore().getContact(recipient.getNumber().get());
+        ContactInfo contact = account.getContactStore().getContact(recipient);
         if (contact == null || contact.profileKey == null) {
             return null;
         }
@@ -1005,7 +1110,7 @@ public class Manager implements Signal {
     /**
      * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
      */
-    private void sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection<String> recipients)
+    private void sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients)
             throws EncapsulatedExceptions, IOException {
         List<SendMessageResult> results = sendMessage(messageBuilder, recipients);
 
@@ -1027,14 +1132,24 @@ public class Manager implements Signal {
         }
     }
 
-    private List<SendMessageResult> sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection<String> recipients)
-            throws IOException {
-        Set<SignalServiceAddress> recipientsTS = Utils.getSignalServiceAddresses(recipients, username);
-        if (recipientsTS == null) {
-            account.save();
-            return Collections.emptyList();
+    private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
+        final Set<SignalServiceAddress> signalServiceAddresses = new HashSet<>(numbers.size());
+        final String username = account.getUsername();
+
+        for (String number : numbers) {
+            String canonicalizedNumber = Utils.canonicalizeNumber(number, username);
+            if (canonicalizedNumber.equals(username)) {
+                signalServiceAddresses.add(account.getSelfAddress());
+            } else {
+                // TODO get corresponding uuid
+                signalServiceAddresses.add(new SignalServiceAddress(null, canonicalizedNumber));
+            }
         }
+        return signalServiceAddresses;
+    }
 
+    private List<SendMessageResult> sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients)
+            throws IOException {
         if (messagePipe == null) {
             messagePipe = getMessageReceiver().createMessagePipe();
         }
@@ -1049,7 +1164,7 @@ public class Manager implements Signal {
             if (message.getGroupInfo().isPresent()) {
                 try {
                     final boolean isRecipientUpdate = false;
-                    List<SendMessageResult> result = messageSender.sendMessage(new ArrayList<>(recipientsTS), getAccessFor(recipientsTS), isRecipientUpdate, message);
+                    List<SendMessageResult> result = messageSender.sendMessage(new ArrayList<>(recipients), getAccessFor(recipients), isRecipientUpdate, message);
                     for (SendMessageResult r : result) {
                         if (r.getIdentityFailure() != null) {
                             account.getSignalProtocolStore().saveIdentity(r.getAddress().getNumber().get(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED);
@@ -1060,8 +1175,8 @@ public class Manager implements Signal {
                     account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
                     return Collections.emptyList();
                 }
-            } else if (recipientsTS.size() == 1 && recipientsTS.contains(getSelfAddress())) {
-                SignalServiceAddress recipient = getSelfAddress();
+            } else if (recipients.size() == 1 && recipients.contains(account.getSelfAddress())) {
+                SignalServiceAddress recipient = account.getSelfAddress();
                 final Optional<UnidentifiedAccessPair> unidentifiedAccess = getAccessFor(recipient);
                 SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(recipient),
                         message.getTimestamp(),
@@ -1071,7 +1186,7 @@ public class Manager implements Signal {
                         false);
                 SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
 
-                List<SendMessageResult> results = new ArrayList<>(recipientsTS.size());
+                List<SendMessageResult> results = new ArrayList<>(recipients.size());
                 try {
                     messageSender.sendMessage(syncMessage, unidentifiedAccess);
                 } catch (UntrustedIdentityException e) {
@@ -1081,11 +1196,11 @@ public class Manager implements Signal {
                 return results;
             } else {
                 // Send to all individually, so sync messages are sent correctly
-                List<SendMessageResult> results = new ArrayList<>(recipientsTS.size());
-                for (SignalServiceAddress address : recipientsTS) {
-                    ThreadInfo thread = account.getThreadStore().getThread(address.getNumber().get());
-                    if (thread != null) {
-                        messageBuilder.withExpiration(thread.messageExpirationTime);
+                List<SendMessageResult> results = new ArrayList<>(recipients.size());
+                for (SignalServiceAddress address : recipients) {
+                    ContactInfo contact = account.getContactStore().getContact(address);
+                    if (contact != null) {
+                        messageBuilder.withExpiration(contact.messageExpirationTime);
                     } else {
                         messageBuilder.withExpiration(0);
                     }
@@ -1102,7 +1217,7 @@ public class Manager implements Signal {
             }
         } finally {
             if (message != null && message.isEndSession()) {
-                for (SignalServiceAddress recipient : recipientsTS) {
+                for (SignalServiceAddress recipient : recipients) {
                     handleEndSession(recipient.getNumber().get());
                 }
             }
@@ -1111,7 +1226,7 @@ public class Manager implements Signal {
     }
 
     private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException, UnsupportedDataMessageException {
-        SignalServiceCipher cipher = new SignalServiceCipher(getSelfAddress(), account.getSignalProtocolStore(), Utils.getCertificateValidator());
+        SignalServiceCipher cipher = new SignalServiceCipher(account.getSelfAddress(), account.getSignalProtocolStore(), Utils.getCertificateValidator());
         try {
             return cipher.decrypt(envelope);
         } catch (ProtocolUntrustedIdentityException e) {
@@ -1125,11 +1240,9 @@ public class Manager implements Signal {
         account.getSignalProtocolStore().deleteAllSessions(source);
     }
 
-    private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, SignalServiceAddress destination, boolean ignoreAttachments) {
-        String threadId;
+    private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments) {
         if (message.getGroupInfo().isPresent()) {
             SignalServiceGroup groupInfo = message.getGroupInfo().get();
-            threadId = Base64.encodeBytes(groupInfo.getGroupId());
             GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId());
             switch (groupInfo.getType()) {
                 case UPDATE:
@@ -1175,7 +1288,7 @@ public class Manager implements Signal {
                             e.printStackTrace();
                         }
                     } else {
-                        group.members.remove(source);
+                        group.removeMember(source);
                         account.getGroupStore().updateGroup(group);
                     }
                     break;
@@ -1191,25 +1304,30 @@ public class Manager implements Signal {
                     }
                     break;
             }
-        } else {
-            if (isSync) {
-                threadId = destination.getNumber().get();
-            } else {
-                threadId = source;
-            }
         }
         if (message.isEndSession()) {
-            handleEndSession(isSync ? destination.getNumber().get() : source);
+            handleEndSession(isSync ? destination.getNumber().get() : source.getNumber().get());
         }
         if (message.isExpirationUpdate() || message.getBody().isPresent()) {
-            ThreadInfo thread = account.getThreadStore().getThread(threadId);
-            if (thread == null) {
-                thread = new ThreadInfo();
-                thread.id = threadId;
-            }
-            if (thread.messageExpirationTime != message.getExpiresInSeconds()) {
-                thread.messageExpirationTime = message.getExpiresInSeconds();
-                account.getThreadStore().updateThread(thread);
+            if (message.getGroupInfo().isPresent()) {
+                SignalServiceGroup groupInfo = message.getGroupInfo().get();
+                GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId());
+                if (group == null) {
+                    group = new GroupInfo(groupInfo.getGroupId());
+                }
+                if (group.messageExpirationTime != message.getExpiresInSeconds()) {
+                    group.messageExpirationTime = message.getExpiresInSeconds();
+                    account.getGroupStore().updateGroup(group);
+                }
+            } else {
+                ContactInfo contact = account.getContactStore().getContact(isSync ? destination : source);
+                if (contact == null) {
+                    contact = new ContactInfo(isSync ? destination : source);
+                }
+                if (contact.messageExpirationTime != message.getExpiresInSeconds()) {
+                    contact.messageExpirationTime = message.getExpiresInSeconds();
+                    account.getContactStore().updateContact(contact);
+                }
             }
         }
         if (message.getAttachments().isPresent() && !ignoreAttachments) {
@@ -1224,7 +1342,7 @@ public class Manager implements Signal {
             }
         }
         if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
-            if (source.equals(username)) {
+            if (source.equals(account.getSelfAddress())) {
                 try {
                     this.account.setProfileKey(new ProfileKey(message.getProfileKey().get()));
                 } catch (InvalidInputException ignored) {
@@ -1232,10 +1350,10 @@ public class Manager implements Signal {
             }
             ContactInfo contact = account.getContactStore().getContact(source);
             if (contact == null) {
-                contact = new ContactInfo();
-                contact.number = source;
+                contact = new ContactInfo(source);
             }
             contact.profileKey = Base64.encodeBytes(message.getProfileKey().get());
+            account.getContactStore().updateContact(contact);
         }
         if (message.getPreviews().isPresent()) {
             final List<SignalServiceDataMessage.Preview> previews = message.getPreviews().get();
@@ -1399,14 +1517,23 @@ public class Manager implements Signal {
             }
             if (content.getDataMessage().isPresent()) {
                 SignalServiceDataMessage message = content.getDataMessage().get();
-                handleSignalServiceDataMessage(message, false, sender.getNumber().get(), getSelfAddress(), ignoreAttachments);
+
+                if (content.isNeedsReceipt()) {
+                    try {
+                        sendReceipt(sender, message.getTimestamp());
+                    } catch (IOException | UntrustedIdentityException e) {
+                        e.printStackTrace();
+                    }
+                }
+
+                handleSignalServiceDataMessage(message, false, sender, account.getSelfAddress(), ignoreAttachments);
             }
             if (content.getSyncMessage().isPresent()) {
                 account.setMultiDevice(true);
                 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
                 if (syncMessage.getSent().isPresent()) {
                     SentTranscriptMessage message = syncMessage.getSent().get();
-                    handleSignalServiceDataMessage(message.getMessage(), true, sender.getNumber().get(), message.getDestination().orNull(), ignoreAttachments);
+                    handleSignalServiceDataMessage(message.getMessage(), true, sender, message.getDestination().orNull(), ignoreAttachments);
                 }
                 if (syncMessage.getRequest().isPresent()) {
                     RequestMessage rm = syncMessage.getRequest().get();
@@ -1449,7 +1576,12 @@ public class Manager implements Signal {
                                     syncGroup.name = g.getName().get();
                                 }
                                 syncGroup.addMembers(g.getMembers());
-                                syncGroup.active = g.isActive();
+                                if (!g.isActive()) {
+                                    syncGroup.removeMember(account.getSelfAddress());
+                                } else {
+                                    // Add ourself to the member set as it's marked as active
+                                    syncGroup.addMembers(Collections.singleton(account.getSelfAddress()));
+                                }
                                 syncGroup.blocked = g.isBlocked();
                                 if (g.getColor().isPresent()) {
                                     syncGroup.color = g.getColor().get();
@@ -1509,10 +1641,9 @@ public class Manager implements Signal {
                                 if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
                                     account.setProfileKey(c.getProfileKey().get());
                                 }
-                                ContactInfo contact = account.getContactStore().getContact(c.getAddress().getNumber().get());
+                                ContactInfo contact = account.getContactStore().getContact(c.getAddress());
                                 if (contact == null) {
-                                    contact = new ContactInfo();
-                                    contact.number = c.getAddress().getNumber().get();
+                                    contact = new ContactInfo(c.getAddress());
                                 }
                                 if (c.getName().isPresent()) {
                                     contact.name = c.getName().get();
@@ -1528,13 +1659,7 @@ public class Manager implements Signal {
                                     account.getSignalProtocolStore().saveIdentity(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
                                 }
                                 if (c.getExpirationTimer().isPresent()) {
-                                    ThreadInfo thread = account.getThreadStore().getThread(c.getAddress().getNumber().get());
-                                    if (thread == null) {
-                                        thread = new ThreadInfo();
-                                        thread.id = c.getAddress().getNumber().get();
-                                    }
-                                    thread.messageExpirationTime = c.getExpirationTimer().get();
-                                    account.getThreadStore().updateThread(thread);
+                                    contact.messageExpirationTime = c.getExpirationTimer().get();
                                 }
                                 contact.blocked = c.isBlocked();
                                 contact.inboxPosition = c.getInboxPosition().orNull();
@@ -1662,10 +1787,9 @@ public class Manager implements Signal {
             try (OutputStream fos = new FileOutputStream(groupsFile)) {
                 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos);
                 for (GroupInfo record : account.getGroupStore().getGroups()) {
-                    ThreadInfo info = account.getThreadStore().getThread(Base64.encodeBytes(record.groupId));
                     out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
                             new ArrayList<>(record.getMembers()), createGroupAvatarAttachment(record.groupId),
-                            record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null),
+                            record.isMember(account.getSelfAddress()), Optional.of(record.messageExpirationTime),
                             Optional.fromNullable(record.color), record.blocked, Optional.fromNullable(record.inboxPosition), record.archived));
                 }
             }
@@ -1698,7 +1822,6 @@ public class Manager implements Signal {
                 DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos);
                 for (ContactInfo record : account.getContactStore().getContacts()) {
                     VerifiedMessage verifiedMessage = null;
-                    ThreadInfo info = account.getThreadStore().getThread(record.number);
                     if (getIdentities().containsKey(record.number)) {
                         JsonIdentityKeyStore.Identity currentIdentity = null;
                         for (JsonIdentityKeyStore.Identity id : getIdentities().get(record.number)) {
@@ -1719,7 +1842,7 @@ public class Manager implements Signal {
                     out.write(new DeviceContact(record.getAddress(), Optional.fromNullable(record.name),
                             createContactAvatarAttachment(record.number), Optional.fromNullable(record.color),
                             Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), record.blocked,
-                            Optional.fromNullable(info != null ? info.messageExpirationTime : null),
+                            Optional.of(record.messageExpirationTime),
                             Optional.fromNullable(record.inboxPosition), record.archived));
                 }
 
@@ -1779,7 +1902,7 @@ public class Manager implements Signal {
     }
 
     public ContactInfo getContact(String number) {
-        return account.getContactStore().getContact(number);
+        return account.getContactStore().getContact(new SignalServiceAddress(null, number));
     }
 
     public GroupInfo getGroup(byte[] groupId) {
@@ -1791,7 +1914,7 @@ public class Manager implements Signal {
     }
 
     public Pair<String, List<JsonIdentityKeyStore.Identity>> getIdentities(String number) throws InvalidNumberException {
-        String canonicalizedNumber = Utils.canonicalizeNumber(number, username);
+        String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername());
         return new Pair<>(canonicalizedNumber, account.getSignalProtocolStore().getIdentities(canonicalizedNumber));
     }
 
@@ -1876,7 +1999,7 @@ public class Manager implements Signal {
     }
 
     public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) {
-        return Utils.computeSafetyNumber(username, getIdentity(), theirUsername, theirIdentityKey);
+        return Utils.computeSafetyNumber(account.getUsername(), getIdentity(), theirUsername, theirIdentityKey);
     }
 
     public interface ReceiveMessageHandler {