]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
Implement device linking
[signal-cli] / src / main / java / org / asamk / signal / Manager.java
1 /**
2 * Copyright (C) 2015 AsamK
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17 package org.asamk.signal;
18
19 import com.fasterxml.jackson.annotation.JsonAutoDetect;
20 import com.fasterxml.jackson.annotation.PropertyAccessor;
21 import com.fasterxml.jackson.databind.DeserializationFeature;
22 import com.fasterxml.jackson.databind.JsonNode;
23 import com.fasterxml.jackson.databind.ObjectMapper;
24 import com.fasterxml.jackson.databind.SerializationFeature;
25 import com.fasterxml.jackson.databind.node.ObjectNode;
26 import org.asamk.Signal;
27 import org.whispersystems.libsignal.*;
28 import org.whispersystems.libsignal.ecc.Curve;
29 import org.whispersystems.libsignal.ecc.ECKeyPair;
30 import org.whispersystems.libsignal.state.PreKeyRecord;
31 import org.whispersystems.libsignal.state.SignalProtocolStore;
32 import org.whispersystems.libsignal.state.SignedPreKeyRecord;
33 import org.whispersystems.libsignal.util.KeyHelper;
34 import org.whispersystems.libsignal.util.Medium;
35 import org.whispersystems.libsignal.util.guava.Optional;
36 import org.whispersystems.signalservice.api.SignalServiceAccountManager;
37 import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
38 import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
39 import org.whispersystems.signalservice.api.SignalServiceMessageSender;
40 import org.whispersystems.signalservice.api.crypto.*;
41 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
42 import org.whispersystems.signalservice.api.messages.*;
43 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
44 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
45 import org.whispersystems.signalservice.api.push.TrustStore;
46 import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
47 import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
48 import org.whispersystems.signalservice.api.util.InvalidNumberException;
49 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
50
51 import java.io.*;
52 import java.net.URI;
53 import java.net.URISyntaxException;
54 import java.net.URLEncoder;
55 import java.nio.file.Files;
56 import java.nio.file.Paths;
57 import java.util.*;
58 import java.util.concurrent.TimeUnit;
59 import java.util.concurrent.TimeoutException;
60
61 class Manager implements Signal {
62 private final static String URL = "https://textsecure-service.whispersystems.org";
63 private final static TrustStore TRUST_STORE = new WhisperTrustStore();
64
65 public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle();
66 public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion();
67 private final static String USER_AGENT = PROJECT_NAME == null ? null : PROJECT_NAME + " " + PROJECT_VERSION;
68
69 private final static int PREKEY_MINIMUM_COUNT = 20;
70 private static final int PREKEY_BATCH_SIZE = 100;
71
72 private final String settingsPath;
73 private final String dataPath;
74 private final String attachmentsPath;
75
76 private final ObjectMapper jsonProcessot = new ObjectMapper();
77 private String username;
78 int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
79 private String password;
80 private String signalingKey;
81 private int preKeyIdOffset;
82 private int nextSignedPreKeyId;
83
84 private boolean registered = false;
85
86 private SignalProtocolStore signalProtocolStore;
87 private SignalServiceAccountManager accountManager;
88 private JsonGroupStore groupStore;
89
90 public Manager(String username, String settingsPath) {
91 this.username = username;
92 this.settingsPath = settingsPath;
93 this.dataPath = this.settingsPath + "/data";
94 this.attachmentsPath = this.settingsPath + "/attachments";
95
96 jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
97 jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
98 jsonProcessot.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
99 jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
100 }
101
102 public String getUsername() {
103 return username;
104 }
105
106 public String getFileName() {
107 new File(dataPath).mkdirs();
108 return dataPath + "/" + username;
109 }
110
111 public boolean userExists() {
112 if (username == null) {
113 return false;
114 }
115 File f = new File(getFileName());
116 return !(!f.exists() || f.isDirectory());
117 }
118
119 public boolean userHasKeys() {
120 return signalProtocolStore != null;
121 }
122
123 private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
124 JsonNode node = parent.get(name);
125 if (node == null) {
126 throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name));
127 }
128
129 return node;
130 }
131
132 public void load() throws IOException, InvalidKeyException {
133 JsonNode rootNode = jsonProcessot.readTree(new File(getFileName()));
134
135 JsonNode node = rootNode.get("deviceId");
136 if (node != null) {
137 deviceId = node.asInt();
138 }
139 username = getNotNullNode(rootNode, "username").asText();
140 password = getNotNullNode(rootNode, "password").asText();
141 if (rootNode.has("signalingKey")) {
142 signalingKey = getNotNullNode(rootNode, "signalingKey").asText();
143 }
144 if (rootNode.has("preKeyIdOffset")) {
145 preKeyIdOffset = getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
146 } else {
147 preKeyIdOffset = 0;
148 }
149 if (rootNode.has("nextSignedPreKeyId")) {
150 nextSignedPreKeyId = getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
151 } else {
152 nextSignedPreKeyId = 0;
153 }
154 signalProtocolStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
155 registered = getNotNullNode(rootNode, "registered").asBoolean();
156 JsonNode groupStoreNode = rootNode.get("groupStore");
157 if (groupStoreNode != null) {
158 groupStore = jsonProcessot.convertValue(groupStoreNode, JsonGroupStore.class);
159 }
160 if (groupStore == null) {
161 groupStore = new JsonGroupStore();
162 }
163 accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT);
164 try {
165 if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) {
166 refreshPreKeys();
167 save();
168 }
169 } catch (AuthorizationFailedException e) {
170 System.err.println("Authorization failed, was the number registered elsewhere?");
171 }
172 }
173
174 private void save() {
175 ObjectNode rootNode = jsonProcessot.createObjectNode();
176 rootNode.put("username", username)
177 .put("deviceId", deviceId)
178 .put("password", password)
179 .put("signalingKey", signalingKey)
180 .put("preKeyIdOffset", preKeyIdOffset)
181 .put("nextSignedPreKeyId", nextSignedPreKeyId)
182 .put("registered", registered)
183 .putPOJO("axolotlStore", signalProtocolStore)
184 .putPOJO("groupStore", groupStore)
185 ;
186 try {
187 jsonProcessot.writeValue(new File(getFileName()), rootNode);
188 } catch (Exception e) {
189 System.err.println(String.format("Error saving file: %s", e.getMessage()));
190 }
191 }
192
193 public void createNewIdentity() {
194 IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
195 int registrationId = KeyHelper.generateRegistrationId(false);
196 signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
197 groupStore = new JsonGroupStore();
198 registered = false;
199 save();
200 }
201
202 public boolean isRegistered() {
203 return registered;
204 }
205
206 public void register(boolean voiceVerication) throws IOException {
207 password = Util.getSecret(18);
208
209 accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
210
211 if (voiceVerication)
212 accountManager.requestVoiceVerificationCode();
213 else
214 accountManager.requestSmsVerificationCode();
215
216 registered = false;
217 save();
218 }
219
220 public URI getDeviceLinkUri() throws TimeoutException, IOException {
221 password = Util.getSecret(18);
222
223 accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
224 String uuid = accountManager.getNewDeviceUuid();
225
226 registered = false;
227 try {
228 return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(signalProtocolStore.getIdentityKeyPair().getPublicKey().serialize()), "utf-8"));
229 } catch (URISyntaxException e) {
230 // Shouldn't happen
231 return null;
232 }
233 }
234
235 public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists {
236 signalingKey = Util.getSecret(52);
237 SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(signalProtocolStore.getIdentityKeyPair(), signalingKey, false, true, signalProtocolStore.getLocalRegistrationId(), deviceName);
238 deviceId = ret.getDeviceId();
239 username = ret.getNumber();
240 // TODO do this check before actually registering
241 if (userExists()) {
242 throw new UserAlreadyExists(username, getFileName());
243 }
244 signalProtocolStore = new JsonSignalProtocolStore(ret.getIdentity(), signalProtocolStore.getLocalRegistrationId());
245
246 registered = true;
247 refreshPreKeys();
248 }
249
250 private List<PreKeyRecord> generatePreKeys() {
251 List<PreKeyRecord> records = new LinkedList<>();
252
253 for (int i = 0; i < PREKEY_BATCH_SIZE; i++) {
254 int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
255 ECKeyPair keyPair = Curve.generateKeyPair();
256 PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
257
258 signalProtocolStore.storePreKey(preKeyId, record);
259 records.add(record);
260 }
261
262 preKeyIdOffset = (preKeyIdOffset + PREKEY_BATCH_SIZE + 1) % Medium.MAX_VALUE;
263 save();
264
265 return records;
266 }
267
268 private PreKeyRecord getOrGenerateLastResortPreKey() {
269 if (signalProtocolStore.containsPreKey(Medium.MAX_VALUE)) {
270 try {
271 return signalProtocolStore.loadPreKey(Medium.MAX_VALUE);
272 } catch (InvalidKeyIdException e) {
273 signalProtocolStore.removePreKey(Medium.MAX_VALUE);
274 }
275 }
276
277 ECKeyPair keyPair = Curve.generateKeyPair();
278 PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
279
280 signalProtocolStore.storePreKey(Medium.MAX_VALUE, record);
281 save();
282
283 return record;
284 }
285
286 private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
287 try {
288 ECKeyPair keyPair = Curve.generateKeyPair();
289 byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
290 SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
291
292 signalProtocolStore.storeSignedPreKey(nextSignedPreKeyId, record);
293 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
294 save();
295
296 return record;
297 } catch (InvalidKeyException e) {
298 throw new AssertionError(e);
299 }
300 }
301
302 public void verifyAccount(String verificationCode) throws IOException {
303 verificationCode = verificationCode.replace("-", "");
304 signalingKey = Util.getSecret(52);
305 accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, true);
306
307 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
308 registered = true;
309
310 refreshPreKeys();
311 save();
312 }
313
314 private void refreshPreKeys() throws IOException {
315 List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
316 PreKeyRecord lastResortKey = getOrGenerateLastResortPreKey();
317 SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair());
318
319 accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys);
320 }
321
322
323 private static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
324 List<SignalServiceAttachment> SignalServiceAttachments = null;
325 if (attachments != null) {
326 SignalServiceAttachments = new ArrayList<>(attachments.size());
327 for (String attachment : attachments) {
328 try {
329 SignalServiceAttachments.add(createAttachment(attachment));
330 } catch (IOException e) {
331 throw new AttachmentInvalidException(attachment, e);
332 }
333 }
334 }
335 return SignalServiceAttachments;
336 }
337
338 private static SignalServiceAttachmentStream createAttachment(String attachment) throws IOException {
339 File attachmentFile = new File(attachment);
340 InputStream attachmentStream = new FileInputStream(attachmentFile);
341 final long attachmentSize = attachmentFile.length();
342 String mime = Files.probeContentType(Paths.get(attachment));
343 return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null);
344 }
345
346 @Override
347 public void sendGroupMessage(String messageText, List<String> attachments,
348 byte[] groupId)
349 throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException {
350 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
351 if (attachments != null) {
352 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
353 }
354 if (groupId != null) {
355 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
356 .withId(groupId)
357 .build();
358 messageBuilder.asGroupMessage(group);
359 }
360 SignalServiceDataMessage message = messageBuilder.build();
361
362 sendMessage(message, groupStore.getGroup(groupId).members);
363 }
364
365 public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException {
366 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
367 .withId(groupId)
368 .build();
369
370 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
371 .asGroupMessage(group)
372 .build();
373
374 sendMessage(message, groupStore.getGroup(groupId).members);
375 }
376
377 public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException {
378 GroupInfo g;
379 if (groupId == null) {
380 // Create new group
381 g = new GroupInfo(Util.getSecretBytes(16));
382 g.members.add(username);
383 } else {
384 g = groupStore.getGroup(groupId);
385 }
386
387 if (name != null) {
388 g.name = name;
389 }
390
391 if (members != null) {
392 for (String member : members) {
393 try {
394 g.members.add(canonicalizeNumber(member));
395 } catch (InvalidNumberException e) {
396 System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
397 System.err.println("Aborting…");
398 System.exit(1);
399 }
400 }
401 }
402
403 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
404 .withId(g.groupId)
405 .withName(g.name)
406 .withMembers(new ArrayList<>(g.members));
407
408 if (avatarFile != null) {
409 try {
410 group.withAvatar(createAttachment(avatarFile));
411 // TODO
412 g.avatarId = 0;
413 } catch (IOException e) {
414 throw new AttachmentInvalidException(avatarFile, e);
415 }
416 }
417
418 groupStore.updateGroup(g);
419
420 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
421 .asGroupMessage(group.build())
422 .build();
423
424 sendMessage(message, g.members);
425 return g.groupId;
426 }
427
428 @Override
429 public void sendMessage(String message, List<String> attachments, String recipient)
430 throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException {
431 List<String> recipients = new ArrayList<>(1);
432 recipients.add(recipient);
433 sendMessage(message, attachments, recipients);
434 }
435
436 @Override
437 public void sendMessage(String messageText, List<String> attachments,
438 List<String> recipients)
439 throws IOException, EncapsulatedExceptions, AttachmentInvalidException, UntrustedIdentityException {
440 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
441 if (attachments != null) {
442 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
443 }
444 SignalServiceDataMessage message = messageBuilder.build();
445
446 sendMessage(message, recipients);
447 }
448
449 @Override
450 public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
451 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
452 .asEndSessionMessage()
453 .build();
454
455 sendMessage(message, recipients);
456 }
457
458 private void sendMessage(SignalServiceDataMessage message, Collection<String> recipients)
459 throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
460 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
461 deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
462
463 Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
464 for (String recipient : recipients) {
465 try {
466 recipientsTS.add(getPushAddress(recipient));
467 } catch (InvalidNumberException e) {
468 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
469 System.err.println("Aborting sending.");
470 save();
471 return;
472 }
473 }
474
475 if (message.getGroupInfo().isPresent()) {
476 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
477 } else {
478 // Send to all individually, so sync messages are sent correctly
479 for (SignalServiceAddress address : recipientsTS) {
480 messageSender.sendMessage(address, message);
481 }
482 }
483
484 if (message.isEndSession()) {
485 for (SignalServiceAddress recipient : recipientsTS) {
486 handleEndSession(recipient.getNumber());
487 }
488 }
489 save();
490 }
491
492 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) {
493 SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore);
494 try {
495 return cipher.decrypt(envelope);
496 } catch (Exception e) {
497 // TODO handle all exceptions
498 e.printStackTrace();
499 return null;
500 }
501 }
502
503 private void handleEndSession(String source) {
504 signalProtocolStore.deleteAllSessions(source);
505 }
506
507 public interface ReceiveMessageHandler {
508 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, GroupInfo group);
509 }
510
511 private GroupInfo handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) {
512 GroupInfo group = null;
513 if (message.getGroupInfo().isPresent()) {
514 SignalServiceGroup groupInfo = message.getGroupInfo().get();
515 switch (groupInfo.getType()) {
516 case UPDATE:
517 try {
518 group = groupStore.getGroup(groupInfo.getGroupId());
519 } catch (GroupNotFoundException e) {
520 group = new GroupInfo(groupInfo.getGroupId());
521 }
522
523 if (groupInfo.getAvatar().isPresent()) {
524 SignalServiceAttachment avatar = groupInfo.getAvatar().get();
525 if (avatar.isPointer()) {
526 long avatarId = avatar.asPointer().getId();
527 try {
528 retrieveAttachment(avatar.asPointer());
529 group.avatarId = avatarId;
530 } catch (IOException | InvalidMessageException e) {
531 System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage());
532 }
533 }
534 }
535
536 if (groupInfo.getName().isPresent()) {
537 group.name = groupInfo.getName().get();
538 }
539
540 if (groupInfo.getMembers().isPresent()) {
541 group.members.addAll(groupInfo.getMembers().get());
542 }
543
544 groupStore.updateGroup(group);
545 break;
546 case DELIVER:
547 try {
548 group = groupStore.getGroup(groupInfo.getGroupId());
549 } catch (GroupNotFoundException e) {
550 }
551 break;
552 case QUIT:
553 try {
554 group = groupStore.getGroup(groupInfo.getGroupId());
555 group.members.remove(source);
556 } catch (GroupNotFoundException e) {
557 }
558 break;
559 }
560 }
561 if (message.isEndSession()) {
562 handleEndSession(isSync ? destination : source);
563 }
564 if (message.getAttachments().isPresent()) {
565 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
566 if (attachment.isPointer()) {
567 try {
568 retrieveAttachment(attachment.asPointer());
569 } catch (IOException | InvalidMessageException e) {
570 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
571 }
572 }
573 }
574 }
575 return group;
576 }
577
578 public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
579 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
580 SignalServiceMessagePipe messagePipe = null;
581
582 try {
583 messagePipe = messageReceiver.createMessagePipe();
584
585 while (true) {
586 SignalServiceEnvelope envelope;
587 SignalServiceContent content = null;
588 GroupInfo group = null;
589 try {
590 envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS);
591 if (!envelope.isReceipt()) {
592 content = decryptMessage(envelope);
593 if (content != null) {
594 if (content.getDataMessage().isPresent()) {
595 SignalServiceDataMessage message = content.getDataMessage().get();
596 group = handleSignalServiceDataMessage(message, false, envelope.getSource(), username);
597 }
598 if (content.getSyncMessage().isPresent()) {
599 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
600 if (syncMessage.getSent().isPresent()) {
601 SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
602 group = handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get());
603 }
604 if (syncMessage.getRequest().isPresent()) {
605 // TODO
606 }
607 if (syncMessage.getGroups().isPresent()) {
608 // TODO
609 }
610 }
611 }
612 }
613 save();
614 handler.handleMessage(envelope, content, group);
615 } catch (TimeoutException e) {
616 if (returnOnTimeout)
617 return;
618 } catch (InvalidVersionException e) {
619 System.err.println("Ignoring error: " + e.getMessage());
620 }
621 }
622 } finally {
623 if (messagePipe != null)
624 messagePipe.shutdown();
625 }
626 }
627
628 public File getAttachmentFile(long attachmentId) {
629 return new File(attachmentsPath, attachmentId + "");
630 }
631
632 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
633 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
634
635 File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
636 InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
637
638 new File(attachmentsPath).mkdirs();
639 File outputFile = getAttachmentFile(pointer.getId());
640 OutputStream output = null;
641 try {
642 output = new FileOutputStream(outputFile);
643 byte[] buffer = new byte[4096];
644 int read;
645
646 while ((read = input.read(buffer)) != -1) {
647 output.write(buffer, 0, read);
648 }
649 } catch (FileNotFoundException e) {
650 e.printStackTrace();
651 return null;
652 } finally {
653 if (output != null) {
654 output.close();
655 output = null;
656 }
657 if (!tmpFile.delete()) {
658 System.err.println("Failed to delete temp file: " + tmpFile);
659 }
660 }
661 if (pointer.getPreview().isPresent()) {
662 File previewFile = new File(outputFile + ".preview");
663 try {
664 output = new FileOutputStream(previewFile);
665 byte[] preview = pointer.getPreview().get();
666 output.write(preview, 0, preview.length);
667 } catch (FileNotFoundException e) {
668 e.printStackTrace();
669 return null;
670 } finally {
671 if (output != null) {
672 output.close();
673 }
674 }
675 }
676 return outputFile;
677 }
678
679 private String canonicalizeNumber(String number) throws InvalidNumberException {
680 String localNumber = username;
681 return PhoneNumberFormatter.formatNumber(number, localNumber);
682 }
683
684 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
685 String e164number = canonicalizeNumber(number);
686 return new SignalServiceAddress(e164number);
687 }
688
689 @Override
690 public boolean isRemote() {
691 return false;
692 }
693 }