X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/947818d3172a4257b6d1a160496f1504d8f514ab..e59ceef6e341393e187dc1dfb5024c76a5b9d16e:/src/main/java/org/asamk/signal/Manager.java diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index eb883bb8..6295f730 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -39,11 +39,10 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessagePipe; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; -import org.whispersystems.signalservice.api.crypto.*; +import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; -import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.messages.multidevice.*; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; @@ -80,7 +79,7 @@ class Manager implements Signal { private final ObjectMapper jsonProcessot = new ObjectMapper(); private String username; - int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; + private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private String password; private String signalingKey; private int preKeyIdOffset; @@ -91,6 +90,7 @@ class Manager implements Signal { private SignalProtocolStore signalProtocolStore; private SignalServiceAccountManager accountManager; private JsonGroupStore groupStore; + private JsonContactsStore contactStore; public Manager(String username, String settingsPath) { this.username = username; @@ -108,6 +108,10 @@ class Manager implements Signal { return username; } + public int getDeviceId() { + return deviceId; + } + public String getFileName() { new File(dataPath).mkdirs(); return dataPath + "/" + username; @@ -165,6 +169,14 @@ class Manager implements Signal { if (groupStore == null) { groupStore = new JsonGroupStore(); } + JsonNode contactStoreNode = rootNode.get("contactStore"); + if (contactStoreNode != null) { + contactStore = jsonProcessot.convertValue(contactStoreNode, JsonContactsStore.class); + } + if (contactStore == null) { + contactStore = new JsonContactsStore(); + } + accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT); try { if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { @@ -177,6 +189,9 @@ class Manager implements Signal { } private void save() { + if (username == null) { + return; + } ObjectNode rootNode = jsonProcessot.createObjectNode(); rootNode.put("username", username) .put("deviceId", deviceId) @@ -187,6 +202,7 @@ class Manager implements Signal { .put("registered", registered) .putPOJO("axolotlStore", signalProtocolStore) .putPOJO("groupStore", groupStore) + .putPOJO("contactStore", contactStore) ; try { jsonProcessot.writeValue(new File(getFileName()), rootNode); @@ -250,9 +266,20 @@ class Manager implements Signal { registered = true; refreshPreKeys(); + + requestSyncGroups(); + requestSyncContacts(); + save(); } + public List getLinkedDevices() throws IOException { + return accountManager.getDevices(); + } + + public void removeLinkedDevices(int deviceId) throws IOException { + accountManager.removeDevice(deviceId); + } public static Map getQueryMap(String query) { String[] params = query.split("&"); @@ -276,7 +303,7 @@ class Manager implements Signal { } public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { - Map query = getQueryMap(linkUri.getQuery()); + Map query = getQueryMap(linkUri.getRawQuery()); String deviceIdentifier = query.get("uuid"); String publicKeyEncoded = query.get("pub_key"); @@ -286,10 +313,10 @@ class Manager implements Signal { ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); - addDeviceLink(deviceIdentifier, deviceKey); + addDevice(deviceIdentifier, deviceKey); } - private void addDeviceLink(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { + private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair(); String verificationCode = accountManager.getNewDeviceVerificationCode(); @@ -384,7 +411,7 @@ class Manager implements Signal { return SignalServiceAttachments; } - private static SignalServiceAttachmentStream createAttachment(String attachment) throws IOException { + private static SignalServiceAttachment createAttachment(String attachment) throws IOException { File attachmentFile = new File(attachment); InputStream attachmentStream = new FileInputStream(attachmentFile); final long attachmentSize = attachmentFile.length(); @@ -408,7 +435,9 @@ class Manager implements Signal { } SignalServiceDataMessage message = messageBuilder.build(); - sendMessage(message, groupStore.getGroup(groupId).members); + Set members = groupStore.getGroup(groupId).members; + members.remove(this.username); + sendMessage(message, members); } public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException { @@ -420,7 +449,11 @@ class Manager implements Signal { .asGroupMessage(group) .build(); - sendMessage(message, groupStore.getGroup(groupId).members); + final GroupInfo g = groupStore.getGroup(groupId); + g.members.remove(this.username); + groupStore.updateGroup(g); + + sendMessage(message, g.members); } public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException { @@ -470,7 +503,9 @@ class Manager implements Signal { .asGroupMessage(group.build()) .build(); - sendMessage(message, g.members); + final Set membersSend = g.members; + membersSend.remove(this.username); + sendMessage(message, membersSend); return g.groupId; } @@ -504,38 +539,72 @@ class Manager implements Signal { sendMessage(message, recipients); } - private void sendMessage(SignalServiceDataMessage message, Collection recipients) + 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)); + try { + sendMessage(message); + } catch (EncapsulatedExceptions encapsulatedExceptions) { + encapsulatedExceptions.printStackTrace(); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); + } + } + + private void requestSyncContacts() throws IOException { + SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build(); + SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + try { + sendMessage(message); + } catch (EncapsulatedExceptions encapsulatedExceptions) { + encapsulatedExceptions.printStackTrace(); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); + } + } + + private void sendMessage(SignalServiceSyncMessage message) throws IOException, EncapsulatedExceptions, UntrustedIdentityException { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); + messageSender.sendMessage(message); + } - Set recipientsTS = new HashSet<>(recipients.size()); - for (String recipient : recipients) { - try { - recipientsTS.add(getPushAddress(recipient)); - } catch (InvalidNumberException e) { - System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - save(); - return; + private void sendMessage(SignalServiceDataMessage message, Collection recipients) + throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + try { + SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, + deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); + + Set recipientsTS = new HashSet<>(recipients.size()); + for (String recipient : recipients) { + try { + recipientsTS.add(getPushAddress(recipient)); + } catch (InvalidNumberException e) { + System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); + System.err.println("Aborting sending."); + save(); + return; + } } - } - if (message.getGroupInfo().isPresent()) { - messageSender.sendMessage(new ArrayList<>(recipientsTS), message); - } else { - // Send to all individually, so sync messages are sent correctly - for (SignalServiceAddress address : recipientsTS) { - messageSender.sendMessage(address, message); + if (message.getGroupInfo().isPresent()) { + messageSender.sendMessage(new ArrayList<>(recipientsTS), message); + } else { + // Send to all individually, so sync messages are sent correctly + for (SignalServiceAddress address : recipientsTS) { + messageSender.sendMessage(address, message); + } } - } - if (message.isEndSession()) { - for (SignalServiceAddress recipient : recipientsTS) { - handleEndSession(recipient.getNumber()); + if (message.isEndSession()) { + for (SignalServiceAddress recipient : recipientsTS) { + handleEndSession(recipient.getNumber()); + } } + } finally { + save(); } - save(); } private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) { @@ -575,6 +644,7 @@ class Manager implements Signal { long avatarId = avatar.asPointer().getId(); try { retrieveAttachment(avatar.asPointer()); + // TODO store group avatar in /avatar/groups folder group.avatarId = avatarId; } catch (IOException | InvalidMessageException e) { System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage()); @@ -602,6 +672,7 @@ class Manager implements Signal { try { group = groupStore.getGroup(groupInfo.getGroupId()); group.members.remove(source); + groupStore.updateGroup(group); } catch (GroupNotFoundException e) { } break; @@ -651,10 +722,75 @@ class Manager implements Signal { group = handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get()); } if (syncMessage.getRequest().isPresent()) { - // TODO + RequestMessage rm = syncMessage.getRequest().get(); + if (rm.isContactsRequest()) { + try { + sendContacts(); + } catch (EncapsulatedExceptions encapsulatedExceptions) { + encapsulatedExceptions.printStackTrace(); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); + } + } + if (rm.isGroupsRequest()) { + try { + sendGroups(); + } catch (EncapsulatedExceptions encapsulatedExceptions) { + encapsulatedExceptions.printStackTrace(); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); + } + } } if (syncMessage.getGroups().isPresent()) { - // TODO + try { + DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer())); + DeviceGroup g; + while ((g = s.read()) != null) { + GroupInfo syncGroup; + try { + syncGroup = groupStore.getGroup(g.getId()); + } catch (GroupNotFoundException e) { + syncGroup = new GroupInfo(g.getId()); + } + if (g.getName().isPresent()) { + syncGroup.name = g.getName().get(); + } + syncGroup.members.addAll(g.getMembers()); + syncGroup.active = g.isActive(); + + if (g.getAvatar().isPresent()) { + byte[] ava = new byte[(int) g.getAvatar().get().getLength()]; + org.whispersystems.signalservice.internal.util.Util.readFully(g.getAvatar().get().getInputStream(), ava); + // TODO store group avatar in /avatar/groups folder + } + groupStore.updateGroup(syncGroup); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + if (syncMessage.getContacts().isPresent()) { + try { + DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer())); + DeviceContact c; + while ((c = s.read()) != null) { + ContactInfo contact = new ContactInfo(); + contact.number = c.getNumber(); + if (c.getName().isPresent()) { + contact.name = c.getName().get(); + } + contactStore.updateContact(contact); + + if (c.getAvatar().isPresent()) { + byte[] ava = new byte[(int) c.getAvatar().get().getLength()]; + org.whispersystems.signalservice.internal.util.Util.readFully(c.getAvatar().get().getInputStream(), ava); + // TODO store contact avatar in /avatar/contacts folder + } + } + } catch (Exception e) { + e.printStackTrace(); + } } } } @@ -725,6 +861,14 @@ class Manager implements Signal { return outputFile; } + private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); + File file = File.createTempFile("ts_tmp", "tmp"); + file.deleteOnExit(); + + return messageReceiver.retrieveAttachment(pointer, file); + } + private String canonicalizeNumber(String number) throws InvalidNumberException { String localNumber = username; return PhoneNumberFormatter.formatNumber(number, localNumber); @@ -739,4 +883,63 @@ class Manager implements Signal { public boolean isRemote() { return false; } + + private void sendGroups() throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + File groupsFile = File.createTempFile("multidevice-group-update", ".tmp"); + + try { + DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(groupsFile)); + try { + for (GroupInfo record : groupStore.getGroups()) { + out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), + new ArrayList<>(record.members), Optional.absent(), // TODO + record.active)); + } + } finally { + out.close(); + } + + if (groupsFile.exists() && groupsFile.length() > 0) { + FileInputStream contactsFileStream = new FileInputStream(groupsFile); + SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() + .withStream(contactsFileStream) + .withContentType("application/octet-stream") + .withLength(groupsFile.length()) + .build(); + + sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + } + } finally { + groupsFile.delete(); + } + } + + private void sendContacts() throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp"); + + try { + DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactsFile)); + try { + for (ContactInfo record : contactStore.getContacts()) { + out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), + Optional.absent())); // TODO + } + } finally { + out.close(); + } + + if (contactsFile.exists() && contactsFile.length() > 0) { + FileInputStream contactsFileStream = new FileInputStream(contactsFile); + SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() + .withStream(contactsFileStream) + .withContentType("application/octet-stream") + .withLength(contactsFile.length()) + .build(); + + sendMessage(SignalServiceSyncMessage.forContacts(attachmentStream)); + } + } finally { + contactsFile.delete(); + } + } }