X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/46befdd638dba0bfa20fdfdb389692cb106630dd..bc17f9317e09c97907123da06a44b42b67f16b0d:/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 be94f7f5..90785824 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -58,6 +58,7 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -76,6 +77,7 @@ class Manager implements Signal { private final String settingsPath; private final String dataPath; private final String attachmentsPath; + private final String avatarsPath; private final ObjectMapper jsonProcessot = new ObjectMapper(); private String username; @@ -90,12 +92,14 @@ 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; this.settingsPath = settingsPath; this.dataPath = this.settingsPath + "/data"; this.attachmentsPath = this.settingsPath + "/attachments"; + this.avatarsPath = this.settingsPath + "/avatars"; jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. @@ -168,6 +172,33 @@ class Manager implements Signal { if (groupStore == null) { groupStore = new JsonGroupStore(); } + // Copy group avatars that were previously stored in the attachments folder + // to the new avatar folder + if (groupStore.groupsWithLegacyAvatarId.size() > 0) { + for (GroupInfo g : groupStore.groupsWithLegacyAvatarId) { + File avatarFile = getGroupAvatarFile(g.groupId); + File attachmentFile = getAttachmentFile(g.getAvatarId()); + if (!avatarFile.exists() && attachmentFile.exists()) { + try { + new File(avatarsPath).mkdirs(); + Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + // Ignore + } + } + } + groupStore.groupsWithLegacyAvatarId.clear(); + save(); + } + + 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) { @@ -193,6 +224,7 @@ class Manager implements Signal { .put("registered", registered) .putPOJO("axolotlStore", signalProtocolStore) .putPOJO("groupStore", groupStore) + .putPOJO("contactStore", contactStore) ; try { jsonProcessot.writeValue(new File(getFileName()), rootNode); @@ -392,7 +424,7 @@ class Manager implements Signal { SignalServiceAttachments = new ArrayList<>(attachments.size()); for (String attachment : attachments) { try { - SignalServiceAttachments.add(createAttachment(attachment)); + SignalServiceAttachments.add(createAttachment(new File(attachment))); } catch (IOException e) { throw new AttachmentInvalidException(attachment, e); } @@ -401,14 +433,31 @@ class Manager implements Signal { return SignalServiceAttachments; } - private static SignalServiceAttachment createAttachment(String attachment) throws IOException { - File attachmentFile = new File(attachment); + private static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException { InputStream attachmentStream = new FileInputStream(attachmentFile); final long attachmentSize = attachmentFile.length(); - String mime = Files.probeContentType(Paths.get(attachment)); + String mime = Files.probeContentType(attachmentFile.toPath()); return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null); } + private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { + File file = getGroupAvatarFile(groupId); + if (!file.exists()) { + return Optional.absent(); + } + + return Optional.of(createAttachment(file)); + } + + private Optional createContactAvatarAttachment(String number) throws IOException { + File file = getContactAvatarFile(number); + if (!file.exists()) { + return Optional.absent(); + } + + return Optional.of(createAttachment(file)); + } + @Override public void sendGroupMessage(String messageText, List attachments, byte[] groupId) @@ -425,9 +474,15 @@ class Manager implements Signal { } SignalServiceDataMessage message = messageBuilder.build(); - Set members = groupStore.getGroup(groupId).members; - members.remove(this.username); - sendMessage(message, members); + GroupInfo g = groupStore.getGroup(groupId); + if (g == null) { + throw new GroupNotFoundException(groupId); + } + + // Don't send group message to ourself + final List membersSend = new ArrayList<>(g.members); + membersSend.remove(this.username); + sendMessage(message, membersSend); } public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException { @@ -440,6 +495,9 @@ class Manager implements Signal { .build(); final GroupInfo g = groupStore.getGroup(groupId); + if (g == null) { + throw new GroupNotFoundException(groupId); + } g.members.remove(this.username); groupStore.updateGroup(g); @@ -454,6 +512,9 @@ class Manager implements Signal { g.members.add(username); } else { g = groupStore.getGroup(groupId); + if (g == null) { + throw new GroupNotFoundException(groupId); + } } if (name != null) { @@ -477,11 +538,14 @@ class Manager implements Signal { .withName(g.name) .withMembers(new ArrayList<>(g.members)); + File aFile = getGroupAvatarFile(g.groupId); if (avatarFile != null) { + new File(avatarsPath).mkdirs(); + Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + if (aFile.exists()) { try { - group.withAvatar(createAttachment(avatarFile)); - // TODO - g.avatarId = 0; + group.withAvatar(createAttachment(aFile)); } catch (IOException e) { throw new AttachmentInvalidException(avatarFile, e); } @@ -493,7 +557,8 @@ class Manager implements Signal { .asGroupMessage(group.build()) .build(); - final Set membersSend = g.members; + // Don't send group message to ourself + final List membersSend = new ArrayList<>(g.members); membersSend.remove(this.username); sendMessage(message, membersSend); return g.groupId; @@ -613,31 +678,27 @@ class Manager implements Signal { } public interface ReceiveMessageHandler { - void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, GroupInfo group); + void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent); } - private GroupInfo handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) { - GroupInfo group = null; + private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) { if (message.getGroupInfo().isPresent()) { SignalServiceGroup groupInfo = message.getGroupInfo().get(); switch (groupInfo.getType()) { case UPDATE: - try { - group = groupStore.getGroup(groupInfo.getGroupId()); - } catch (GroupNotFoundException e) { + GroupInfo group; + group = groupStore.getGroup(groupInfo.getGroupId()); + if (group == null) { 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; + retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId); } catch (IOException | InvalidMessageException e) { - System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage()); + System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage()); } } } @@ -653,17 +714,12 @@ class Manager implements Signal { 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 = groupStore.getGroup(groupInfo.getGroupId()); + if (group != null) { group.members.remove(source); groupStore.updateGroup(group); - } catch (GroupNotFoundException e) { } break; } @@ -682,7 +738,6 @@ class Manager implements Signal { } } } - return group; } public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { @@ -695,7 +750,6 @@ class Manager implements Signal { while (true) { SignalServiceEnvelope envelope; SignalServiceContent content = null; - GroupInfo group = null; try { envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS); if (!envelope.isReceipt()) { @@ -703,18 +757,24 @@ class Manager implements Signal { if (content != null) { if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - group = handleSignalServiceDataMessage(message, false, envelope.getSource(), username); + 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()); + 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 + try { + sendContacts(); + } catch (EncapsulatedExceptions encapsulatedExceptions) { + encapsulatedExceptions.printStackTrace(); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); + } } if (rm.isGroupsRequest()) { try { @@ -731,10 +791,8 @@ class Manager implements Signal { 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) { + GroupInfo syncGroup = groupStore.getGroup(g.getId()); + if (syncGroup == null) { syncGroup = new GroupInfo(g.getId()); } if (g.getName().isPresent()) { @@ -744,9 +802,7 @@ class Manager implements Signal { 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 + retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); } groupStore.updateGroup(syncGroup); } @@ -759,16 +815,15 @@ class Manager implements Signal { DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer())); DeviceContact c; while ((c = s.read()) != null) { - // TODO implement when we have contact storage + ContactInfo contact = new ContactInfo(); + contact.number = c.getNumber(); if (c.getName().isPresent()) { - c.getName().get(); + contact.name = c.getName().get(); } - c.getNumber(); + 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 + retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number); } } } catch (Exception e) { @@ -779,7 +834,7 @@ class Manager implements Signal { } } save(); - handler.handleMessage(envelope, content, group); + handler.handleMessage(envelope, content); } catch (TimeoutException e) { if (returnOnTimeout) return; @@ -793,18 +848,48 @@ class Manager implements Signal { } } + public File getContactAvatarFile(String number) { + return new File(avatarsPath, "contact-" + number); + } + + private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException { + new File(avatarsPath).mkdirs(); + if (attachment.isPointer()) { + SignalServiceAttachmentPointer pointer = attachment.asPointer(); + return retrieveAttachment(pointer, getContactAvatarFile(number), false); + } else { + SignalServiceAttachmentStream stream = attachment.asStream(); + return retrieveAttachment(stream, getContactAvatarFile(number)); + } + } + + public File getGroupAvatarFile(byte[] groupId) { + return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_")); + } + + private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException { + new File(avatarsPath).mkdirs(); + if (attachment.isPointer()) { + SignalServiceAttachmentPointer pointer = attachment.asPointer(); + return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false); + } else { + SignalServiceAttachmentStream stream = attachment.asStream(); + return retrieveAttachment(stream, getGroupAvatarFile(groupId)); + } + } + public File getAttachmentFile(long attachmentId) { return new File(attachmentsPath, attachmentId + ""); } private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); + new File(attachmentsPath).mkdirs(); + return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true); + } - File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp"); - InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile); + private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException { + InputStream input = stream.getInputStream(); - new File(attachmentsPath).mkdirs(); - File outputFile = getAttachmentFile(pointer.getId()); OutputStream output = null; try { output = new FileOutputStream(outputFile); @@ -820,14 +905,15 @@ class Manager implements Signal { } finally { if (output != null) { output.close(); - output = null; - } - if (!tmpFile.delete()) { - System.err.println("Failed to delete temp file: " + tmpFile); } } - if (pointer.getPreview().isPresent()) { + return outputFile; + } + + private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException { + if (storePreview && pointer.getPreview().isPresent()) { File previewFile = new File(outputFile + ".preview"); + OutputStream output = null; try { output = new FileOutputStream(previewFile); byte[] preview = pointer.getPreview().get(); @@ -841,6 +927,32 @@ class Manager implements Signal { } } } + + 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); + + OutputStream output = null; + try { + output = new FileOutputStream(outputFile); + byte[] buffer = new byte[4096]; + int read; + + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } finally { + if (output != null) { + output.close(); + } + if (!tmpFile.delete()) { + System.err.println("Failed to delete temp file: " + tmpFile); + } + } return outputFile; } @@ -868,20 +980,49 @@ class Manager implements Signal { } private void sendGroups() throws IOException, EncapsulatedExceptions, UntrustedIdentityException { - File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp"); + File groupsFile = File.createTempFile("multidevice-group-update", ".tmp"); try { - DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(contactsFile)); + 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.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)), + new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId), 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), + createContactAvatarAttachment(record.number))); + } + } finally { + out.close(); + } + if (contactsFile.exists() && contactsFile.length() > 0) { FileInputStream contactsFileStream = new FileInputStream(contactsFile); SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() @@ -890,10 +1031,18 @@ class Manager implements Signal { .withLength(contactsFile.length()) .build(); - sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + sendMessage(SignalServiceSyncMessage.forContacts(attachmentStream)); } } finally { - if (contactsFile != null) contactsFile.delete(); + contactsFile.delete(); } } + + public ContactInfo getContact(String number) { + return contactStore.getContact(number); + } + + public GroupInfo getGroup(byte[] groupId) { + return groupStore.getGroup(groupId); + } }