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