X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/9d18b01d85b0588a03e4640213bb27ae174e7426..15568512b1400d3696e43880a674e5d63f078cd7:/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 d1963cb5..3931f3e3 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -23,10 +23,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.http.util.TextUtils; import org.asamk.Signal; import org.whispersystems.libsignal.*; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; +import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; @@ -38,15 +40,22 @@ import org.whispersystems.signalservice.api.SignalServiceMessagePipe; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; 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.*; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; @@ -70,6 +79,7 @@ class Manager implements Signal { private final ObjectMapper jsonProcessot = new ObjectMapper(); private String username; + private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private String password; private String signalingKey; private int preKeyIdOffset; @@ -93,12 +103,23 @@ class Manager implements Signal { jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); } + public String getUsername() { + return username; + } + + public int getDeviceId() { + return deviceId; + } + public String getFileName() { new File(dataPath).mkdirs(); return dataPath + "/" + username; } public boolean userExists() { + if (username == null) { + return false; + } File f = new File(getFileName()); return !(!f.exists() || f.isDirectory()); } @@ -119,6 +140,10 @@ class Manager implements Signal { public void load() throws IOException, InvalidKeyException { JsonNode rootNode = jsonProcessot.readTree(new File(getFileName())); + JsonNode node = rootNode.get("deviceId"); + if (node != null) { + deviceId = node.asInt(); + } username = getNotNullNode(rootNode, "username").asText(); password = getNotNullNode(rootNode, "password").asText(); if (rootNode.has("signalingKey")) { @@ -143,7 +168,7 @@ class Manager implements Signal { if (groupStore == null) { groupStore = new JsonGroupStore(); } - accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); + accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT); try { if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { refreshPreKeys(); @@ -157,6 +182,7 @@ class Manager implements Signal { private void save() { ObjectNode rootNode = jsonProcessot.createObjectNode(); rootNode.put("username", username) + .put("deviceId", deviceId) .put("password", password) .put("signalingKey", signalingKey) .put("preKeyIdOffset", preKeyIdOffset) @@ -199,6 +225,91 @@ class Manager implements Signal { save(); } + public URI getDeviceLinkUri() throws TimeoutException, IOException { + password = Util.getSecret(18); + + accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); + String uuid = accountManager.getNewDeviceUuid(); + + registered = false; + try { + return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(signalProtocolStore.getIdentityKeyPair().getPublicKey().serialize()), "utf-8")); + } catch (URISyntaxException e) { + // Shouldn't happen + return null; + } + } + + public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { + signalingKey = Util.getSecret(52); + SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(signalProtocolStore.getIdentityKeyPair(), signalingKey, false, true, signalProtocolStore.getLocalRegistrationId(), deviceName); + deviceId = ret.getDeviceId(); + username = ret.getNumber(); + // TODO do this check before actually registering + if (userExists()) { + throw new UserAlreadyExists(username, getFileName()); + } + signalProtocolStore = new JsonSignalProtocolStore(ret.getIdentity(), signalProtocolStore.getLocalRegistrationId()); + + 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("&"); + Map map = new HashMap<>(); + for (String param : params) { + String name = null; + try { + name = URLDecoder.decode(param.split("=")[0], "utf-8"); + } catch (UnsupportedEncodingException e) { + // Impossible + } + String value = null; + try { + value = URLDecoder.decode(param.split("=")[1], "utf-8"); + } catch (UnsupportedEncodingException e) { + // Impossible + } + map.put(name, value); + } + return map; + } + + public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { + Map query = getQueryMap(linkUri.getRawQuery()); + String deviceIdentifier = query.get("uuid"); + String publicKeyEncoded = query.get("pub_key"); + + if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) { + throw new RuntimeException("Invalid device link uri"); + } + + ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); + + addDevice(deviceIdentifier, deviceKey); + } + + private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { + IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair(); + String verificationCode = accountManager.getNewDeviceVerificationCode(); + + accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, verificationCode); + } + private List generatePreKeys() { List records = new LinkedList<>(); @@ -287,7 +398,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(); @@ -298,7 +409,7 @@ class Manager implements Signal { @Override public void sendGroupMessage(String messageText, List attachments, byte[] groupId) - throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); @@ -311,10 +422,12 @@ 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 { + public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupId) .build(); @@ -323,10 +436,14 @@ 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 { + public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException { GroupInfo g; if (groupId == null) { // Create new group @@ -373,13 +490,15 @@ 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; } @Override public void sendMessage(String message, List attachments, String recipient) - throws EncapsulatedExceptions, AttachmentInvalidException, IOException { + throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException { List recipients = new ArrayList<>(1); recipients.add(recipient); sendMessage(message, attachments, recipients); @@ -388,7 +507,7 @@ class Manager implements Signal { @Override public void sendMessage(String messageText, List attachments, List recipients) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException { + throws IOException, EncapsulatedExceptions, AttachmentInvalidException, UntrustedIdentityException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); @@ -399,7 +518,7 @@ class Manager implements Signal { } @Override - public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions { + public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException { SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() .asEndSessionMessage() .build(); @@ -407,31 +526,72 @@ class Manager implements Signal { sendMessage(message, recipients); } - private void sendMessage(SignalServiceDataMessage message, Collection recipients) - throws IOException, EncapsulatedExceptions { + 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, - signalProtocolStore, USER_AGENT, Optional.absent()); + 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; + } } - } - messageSender.sendMessage(new ArrayList<>(recipientsTS), 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) { @@ -453,8 +613,77 @@ class Manager implements Signal { void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, GroupInfo group); } + private GroupInfo handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) { + GroupInfo group = null; + if (message.getGroupInfo().isPresent()) { + SignalServiceGroup groupInfo = message.getGroupInfo().get(); + switch (groupInfo.getType()) { + case UPDATE: + try { + group = groupStore.getGroup(groupInfo.getGroupId()); + } catch (GroupNotFoundException e) { + group = new GroupInfo(groupInfo.getGroupId()); + } + + if (groupInfo.getAvatar().isPresent()) { + SignalServiceAttachment avatar = groupInfo.getAvatar().get(); + if (avatar.isPointer()) { + 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()); + } + } + } + + if (groupInfo.getName().isPresent()) { + group.name = groupInfo.getName().get(); + } + + if (groupInfo.getMembers().isPresent()) { + group.members.addAll(groupInfo.getMembers().get()); + } + + groupStore.updateGroup(group); + break; + case DELIVER: + try { + group = groupStore.getGroup(groupInfo.getGroupId()); + } catch (GroupNotFoundException e) { + } + break; + case QUIT: + try { + group = groupStore.getGroup(groupInfo.getGroupId()); + group.members.remove(source); + groupStore.updateGroup(group); + } catch (GroupNotFoundException e) { + } + break; + } + } + if (message.isEndSession()) { + handleEndSession(isSync ? destination : source); + } + if (message.getAttachments().isPresent()) { + for (SignalServiceAttachment attachment : message.getAttachments().get()) { + if (attachment.isPointer()) { + try { + retrieveAttachment(attachment.asPointer()); + } catch (IOException | InvalidMessageException e) { + System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage()); + } + } + } + } + return group; + } + public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); SignalServiceMessagePipe messagePipe = null; try { @@ -471,66 +700,76 @@ class Manager implements Signal { if (content != null) { if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - if (message.getGroupInfo().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupInfo().get(); - switch (groupInfo.getType()) { - case UPDATE: + group = handleSignalServiceDataMessage(message, false, envelope.getSource(), username); + } + if (content.getSyncMessage().isPresent()) { + SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); + if (syncMessage.getSent().isPresent()) { + SignalServiceDataMessage message = syncMessage.getSent().get().getMessage(); + group = handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get()); + } + if (syncMessage.getRequest().isPresent()) { + RequestMessage rm = syncMessage.getRequest().get(); + if (rm.isContactsRequest()) { + // TODO implement when we have contacts + } + if (rm.isGroupsRequest()) { + try { + sendGroups(); + } catch (EncapsulatedExceptions encapsulatedExceptions) { + encapsulatedExceptions.printStackTrace(); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); + } + } + } + if (syncMessage.getGroups().isPresent()) { + try { + DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer())); + DeviceGroup g; + while ((g = s.read()) != null) { + GroupInfo syncGroup; try { - group = groupStore.getGroup(groupInfo.getGroupId()); + syncGroup = groupStore.getGroup(g.getId()); } catch (GroupNotFoundException e) { - group = new GroupInfo(groupInfo.getGroupId()); + syncGroup = new GroupInfo(g.getId()); } - - if (groupInfo.getAvatar().isPresent()) { - SignalServiceAttachment avatar = groupInfo.getAvatar().get(); - if (avatar.isPointer()) { - long avatarId = avatar.asPointer().getId(); - try { - retrieveAttachment(avatar.asPointer()); - group.avatarId = avatarId; - } catch (IOException | InvalidMessageException e) { - System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage()); - } - } + if (g.getName().isPresent()) { + syncGroup.name = g.getName().get(); } + syncGroup.members.addAll(g.getMembers()); + syncGroup.active = g.isActive(); - if (groupInfo.getName().isPresent()) { - group.name = groupInfo.getName().get(); + 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 } - - if (groupInfo.getMembers().isPresent()) { - group.members.addAll(groupInfo.getMembers().get()); - } - - groupStore.updateGroup(group); - break; - case DELIVER: - try { - group = groupStore.getGroup(groupInfo.getGroupId()); - } catch (GroupNotFoundException e) { - } - break; - case QUIT: - try { - group = groupStore.getGroup(groupInfo.getGroupId()); - group.members.remove(envelope.getSource()); - } catch (GroupNotFoundException e) { - } - break; + groupStore.updateGroup(syncGroup); + } + } catch (Exception e) { + e.printStackTrace(); } } - if (message.isEndSession()) { - handleEndSession(envelope.getSource()); - } - if (message.getAttachments().isPresent()) { - for (SignalServiceAttachment attachment : message.getAttachments().get()) { - if (attachment.isPointer()) { - try { - retrieveAttachment(attachment.asPointer()); - } catch (IOException | InvalidMessageException e) { - System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage()); + if (syncMessage.getContacts().isPresent()) { + try { + DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer())); + DeviceContact c; + while ((c = s.read()) != null) { + // TODO implement when we have contact storage + if (c.getName().isPresent()) { + c.getName().get(); + } + c.getNumber(); + + 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(); } } } @@ -556,7 +795,7 @@ class Manager implements Signal { } private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp"); InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile); @@ -602,6 +841,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); @@ -616,4 +863,34 @@ class Manager implements Signal { public boolean isRemote() { return false; } + + private void sendGroups() throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp"); + + try { + DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(contactsFile)); + try { + for (GroupInfo record : groupStore.getGroups()) { + out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), + new ArrayList<>(record.members), Optional.of(new SignalServiceAttachmentStream(new FileInputStream("/home/sebastian/Bilder/00026_150512_14-00-18.JPG"), "octet", new File("/home/sebastian/Bilder/00026_150512_14-00-18.JPG").length(), null)), + record.active)); + } + } 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.forGroups(attachmentStream)); + } + } finally { + if (contactsFile != null) contactsFile.delete(); + } + } }