]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
Update Readme and fix help bug in Main
[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.SignalServiceCipher;
43 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
44 import org.whispersystems.signalservice.api.messages.*;
45 import org.whispersystems.signalservice.api.messages.multidevice.*;
46 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
47 import org.whispersystems.signalservice.api.push.TrustStore;
48 import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
49 import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
50 import org.whispersystems.signalservice.api.util.InvalidNumberException;
51 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
52 import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
53
54 import java.io.*;
55 import java.net.URI;
56 import java.net.URISyntaxException;
57 import java.net.URLDecoder;
58 import java.net.URLEncoder;
59 import java.nio.file.Files;
60 import java.nio.file.Paths;
61 import java.util.*;
62 import java.util.concurrent.TimeUnit;
63 import java.util.concurrent.TimeoutException;
64
65 class Manager implements Signal {
66 private final static String URL = "https://textsecure-service.whispersystems.org";
67 private final static TrustStore TRUST_STORE = new WhisperTrustStore();
68
69 public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle();
70 public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion();
71 private final static String USER_AGENT = PROJECT_NAME == null ? null : PROJECT_NAME + " " + PROJECT_VERSION;
72
73 private final static int PREKEY_MINIMUM_COUNT = 20;
74 private static final int PREKEY_BATCH_SIZE = 100;
75
76 private final String settingsPath;
77 private final String dataPath;
78 private final String attachmentsPath;
79
80 private final ObjectMapper jsonProcessot = new ObjectMapper();
81 private String username;
82 private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
83 private String password;
84 private String signalingKey;
85 private int preKeyIdOffset;
86 private int nextSignedPreKeyId;
87
88 private boolean registered = false;
89
90 private SignalProtocolStore signalProtocolStore;
91 private SignalServiceAccountManager accountManager;
92 private JsonGroupStore groupStore;
93 private JsonContactsStore contactStore;
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 int getDeviceId() {
112 return deviceId;
113 }
114
115 public String getFileName() {
116 new File(dataPath).mkdirs();
117 return dataPath + "/" + username;
118 }
119
120 public boolean userExists() {
121 if (username == null) {
122 return false;
123 }
124 File f = new File(getFileName());
125 return !(!f.exists() || f.isDirectory());
126 }
127
128 public boolean userHasKeys() {
129 return signalProtocolStore != null;
130 }
131
132 private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
133 JsonNode node = parent.get(name);
134 if (node == null) {
135 throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name));
136 }
137
138 return node;
139 }
140
141 public void load() throws IOException, InvalidKeyException {
142 JsonNode rootNode = jsonProcessot.readTree(new File(getFileName()));
143
144 JsonNode node = rootNode.get("deviceId");
145 if (node != null) {
146 deviceId = node.asInt();
147 }
148 username = getNotNullNode(rootNode, "username").asText();
149 password = getNotNullNode(rootNode, "password").asText();
150 if (rootNode.has("signalingKey")) {
151 signalingKey = getNotNullNode(rootNode, "signalingKey").asText();
152 }
153 if (rootNode.has("preKeyIdOffset")) {
154 preKeyIdOffset = getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
155 } else {
156 preKeyIdOffset = 0;
157 }
158 if (rootNode.has("nextSignedPreKeyId")) {
159 nextSignedPreKeyId = getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
160 } else {
161 nextSignedPreKeyId = 0;
162 }
163 signalProtocolStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
164 registered = getNotNullNode(rootNode, "registered").asBoolean();
165 JsonNode groupStoreNode = rootNode.get("groupStore");
166 if (groupStoreNode != null) {
167 groupStore = jsonProcessot.convertValue(groupStoreNode, JsonGroupStore.class);
168 }
169 if (groupStore == null) {
170 groupStore = new JsonGroupStore();
171 }
172 JsonNode contactStoreNode = rootNode.get("contactStore");
173 if (contactStoreNode != null) {
174 contactStore = jsonProcessot.convertValue(contactStoreNode, JsonContactsStore.class);
175 }
176 if (contactStore == null) {
177 contactStore = new JsonContactsStore();
178 }
179
180 accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT);
181 try {
182 if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) {
183 refreshPreKeys();
184 save();
185 }
186 } catch (AuthorizationFailedException e) {
187 System.err.println("Authorization failed, was the number registered elsewhere?");
188 }
189 }
190
191 private void save() {
192 if (username == null) {
193 return;
194 }
195 ObjectNode rootNode = jsonProcessot.createObjectNode();
196 rootNode.put("username", username)
197 .put("deviceId", deviceId)
198 .put("password", password)
199 .put("signalingKey", signalingKey)
200 .put("preKeyIdOffset", preKeyIdOffset)
201 .put("nextSignedPreKeyId", nextSignedPreKeyId)
202 .put("registered", registered)
203 .putPOJO("axolotlStore", signalProtocolStore)
204 .putPOJO("groupStore", groupStore)
205 .putPOJO("contactStore", contactStore)
206 ;
207 try {
208 jsonProcessot.writeValue(new File(getFileName()), rootNode);
209 } catch (Exception e) {
210 System.err.println(String.format("Error saving file: %s", e.getMessage()));
211 }
212 }
213
214 public void createNewIdentity() {
215 IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
216 int registrationId = KeyHelper.generateRegistrationId(false);
217 signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
218 groupStore = new JsonGroupStore();
219 registered = false;
220 save();
221 }
222
223 public boolean isRegistered() {
224 return registered;
225 }
226
227 public void register(boolean voiceVerication) throws IOException {
228 password = Util.getSecret(18);
229
230 accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
231
232 if (voiceVerication)
233 accountManager.requestVoiceVerificationCode();
234 else
235 accountManager.requestSmsVerificationCode();
236
237 registered = false;
238 save();
239 }
240
241 public URI getDeviceLinkUri() throws TimeoutException, IOException {
242 password = Util.getSecret(18);
243
244 accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
245 String uuid = accountManager.getNewDeviceUuid();
246
247 registered = false;
248 try {
249 return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(signalProtocolStore.getIdentityKeyPair().getPublicKey().serialize()), "utf-8"));
250 } catch (URISyntaxException e) {
251 // Shouldn't happen
252 return null;
253 }
254 }
255
256 public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists {
257 signalingKey = Util.getSecret(52);
258 SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(signalProtocolStore.getIdentityKeyPair(), signalingKey, false, true, signalProtocolStore.getLocalRegistrationId(), deviceName);
259 deviceId = ret.getDeviceId();
260 username = ret.getNumber();
261 // TODO do this check before actually registering
262 if (userExists()) {
263 throw new UserAlreadyExists(username, getFileName());
264 }
265 signalProtocolStore = new JsonSignalProtocolStore(ret.getIdentity(), signalProtocolStore.getLocalRegistrationId());
266
267 registered = true;
268 refreshPreKeys();
269
270 requestSyncGroups();
271 requestSyncContacts();
272
273 save();
274 }
275
276 public List<DeviceInfo> getLinkedDevices() throws IOException {
277 return accountManager.getDevices();
278 }
279
280 public void removeLinkedDevices(int deviceId) throws IOException {
281 accountManager.removeDevice(deviceId);
282 }
283
284 public static Map<String, String> getQueryMap(String query) {
285 String[] params = query.split("&");
286 Map<String, String> map = new HashMap<>();
287 for (String param : params) {
288 String name = null;
289 try {
290 name = URLDecoder.decode(param.split("=")[0], "utf-8");
291 } catch (UnsupportedEncodingException e) {
292 // Impossible
293 }
294 String value = null;
295 try {
296 value = URLDecoder.decode(param.split("=")[1], "utf-8");
297 } catch (UnsupportedEncodingException e) {
298 // Impossible
299 }
300 map.put(name, value);
301 }
302 return map;
303 }
304
305 public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException {
306 Map<String, String> query = getQueryMap(linkUri.getRawQuery());
307 String deviceIdentifier = query.get("uuid");
308 String publicKeyEncoded = query.get("pub_key");
309
310 if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) {
311 throw new RuntimeException("Invalid device link uri");
312 }
313
314 ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
315
316 addDevice(deviceIdentifier, deviceKey);
317 }
318
319 private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException {
320 IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair();
321 String verificationCode = accountManager.getNewDeviceVerificationCode();
322
323 accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, verificationCode);
324 }
325
326 private List<PreKeyRecord> generatePreKeys() {
327 List<PreKeyRecord> records = new LinkedList<>();
328
329 for (int i = 0; i < PREKEY_BATCH_SIZE; i++) {
330 int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
331 ECKeyPair keyPair = Curve.generateKeyPair();
332 PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
333
334 signalProtocolStore.storePreKey(preKeyId, record);
335 records.add(record);
336 }
337
338 preKeyIdOffset = (preKeyIdOffset + PREKEY_BATCH_SIZE + 1) % Medium.MAX_VALUE;
339 save();
340
341 return records;
342 }
343
344 private PreKeyRecord getOrGenerateLastResortPreKey() {
345 if (signalProtocolStore.containsPreKey(Medium.MAX_VALUE)) {
346 try {
347 return signalProtocolStore.loadPreKey(Medium.MAX_VALUE);
348 } catch (InvalidKeyIdException e) {
349 signalProtocolStore.removePreKey(Medium.MAX_VALUE);
350 }
351 }
352
353 ECKeyPair keyPair = Curve.generateKeyPair();
354 PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
355
356 signalProtocolStore.storePreKey(Medium.MAX_VALUE, record);
357 save();
358
359 return record;
360 }
361
362 private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
363 try {
364 ECKeyPair keyPair = Curve.generateKeyPair();
365 byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
366 SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
367
368 signalProtocolStore.storeSignedPreKey(nextSignedPreKeyId, record);
369 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
370 save();
371
372 return record;
373 } catch (InvalidKeyException e) {
374 throw new AssertionError(e);
375 }
376 }
377
378 public void verifyAccount(String verificationCode) throws IOException {
379 verificationCode = verificationCode.replace("-", "");
380 signalingKey = Util.getSecret(52);
381 accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, true);
382
383 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
384 registered = true;
385
386 refreshPreKeys();
387 save();
388 }
389
390 private void refreshPreKeys() throws IOException {
391 List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
392 PreKeyRecord lastResortKey = getOrGenerateLastResortPreKey();
393 SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair());
394
395 accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys);
396 }
397
398
399 private static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
400 List<SignalServiceAttachment> SignalServiceAttachments = null;
401 if (attachments != null) {
402 SignalServiceAttachments = new ArrayList<>(attachments.size());
403 for (String attachment : attachments) {
404 try {
405 SignalServiceAttachments.add(createAttachment(attachment));
406 } catch (IOException e) {
407 throw new AttachmentInvalidException(attachment, e);
408 }
409 }
410 }
411 return SignalServiceAttachments;
412 }
413
414 private static SignalServiceAttachment createAttachment(String attachment) throws IOException {
415 File attachmentFile = new File(attachment);
416 InputStream attachmentStream = new FileInputStream(attachmentFile);
417 final long attachmentSize = attachmentFile.length();
418 String mime = Files.probeContentType(Paths.get(attachment));
419 return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null);
420 }
421
422 @Override
423 public void sendGroupMessage(String messageText, List<String> attachments,
424 byte[] groupId)
425 throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException {
426 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
427 if (attachments != null) {
428 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
429 }
430 if (groupId != null) {
431 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
432 .withId(groupId)
433 .build();
434 messageBuilder.asGroupMessage(group);
435 }
436 SignalServiceDataMessage message = messageBuilder.build();
437
438 Set<String> members = groupStore.getGroup(groupId).members;
439 members.remove(this.username);
440 sendMessage(message, members);
441 }
442
443 public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException {
444 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
445 .withId(groupId)
446 .build();
447
448 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
449 .asGroupMessage(group)
450 .build();
451
452 final GroupInfo g = groupStore.getGroup(groupId);
453 g.members.remove(this.username);
454 groupStore.updateGroup(g);
455
456 sendMessage(message, g.members);
457 }
458
459 public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException {
460 GroupInfo g;
461 if (groupId == null) {
462 // Create new group
463 g = new GroupInfo(Util.getSecretBytes(16));
464 g.members.add(username);
465 } else {
466 g = groupStore.getGroup(groupId);
467 }
468
469 if (name != null) {
470 g.name = name;
471 }
472
473 if (members != null) {
474 for (String member : members) {
475 try {
476 g.members.add(canonicalizeNumber(member));
477 } catch (InvalidNumberException e) {
478 System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
479 System.err.println("Aborting…");
480 System.exit(1);
481 }
482 }
483 }
484
485 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
486 .withId(g.groupId)
487 .withName(g.name)
488 .withMembers(new ArrayList<>(g.members));
489
490 if (avatarFile != null) {
491 try {
492 group.withAvatar(createAttachment(avatarFile));
493 // TODO
494 g.avatarId = 0;
495 } catch (IOException e) {
496 throw new AttachmentInvalidException(avatarFile, e);
497 }
498 }
499
500 groupStore.updateGroup(g);
501
502 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
503 .asGroupMessage(group.build())
504 .build();
505
506 final Set<String> membersSend = g.members;
507 membersSend.remove(this.username);
508 sendMessage(message, membersSend);
509 return g.groupId;
510 }
511
512 @Override
513 public void sendMessage(String message, List<String> attachments, String recipient)
514 throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException {
515 List<String> recipients = new ArrayList<>(1);
516 recipients.add(recipient);
517 sendMessage(message, attachments, recipients);
518 }
519
520 @Override
521 public void sendMessage(String messageText, List<String> attachments,
522 List<String> recipients)
523 throws IOException, EncapsulatedExceptions, AttachmentInvalidException, UntrustedIdentityException {
524 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
525 if (attachments != null) {
526 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
527 }
528 SignalServiceDataMessage message = messageBuilder.build();
529
530 sendMessage(message, recipients);
531 }
532
533 @Override
534 public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
535 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
536 .asEndSessionMessage()
537 .build();
538
539 sendMessage(message, recipients);
540 }
541
542 private void requestSyncGroups() throws IOException {
543 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
544 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
545 try {
546 sendMessage(message);
547 } catch (EncapsulatedExceptions encapsulatedExceptions) {
548 encapsulatedExceptions.printStackTrace();
549 } catch (UntrustedIdentityException e) {
550 e.printStackTrace();
551 }
552 }
553
554 private void requestSyncContacts() throws IOException {
555 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
556 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
557 try {
558 sendMessage(message);
559 } catch (EncapsulatedExceptions encapsulatedExceptions) {
560 encapsulatedExceptions.printStackTrace();
561 } catch (UntrustedIdentityException e) {
562 e.printStackTrace();
563 }
564 }
565
566 private void sendMessage(SignalServiceSyncMessage message)
567 throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
568 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
569 deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
570 messageSender.sendMessage(message);
571 }
572
573 private void sendMessage(SignalServiceDataMessage message, Collection<String> recipients)
574 throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
575 try {
576 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
577 deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
578
579 Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
580 for (String recipient : recipients) {
581 try {
582 recipientsTS.add(getPushAddress(recipient));
583 } catch (InvalidNumberException e) {
584 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
585 System.err.println("Aborting sending.");
586 save();
587 return;
588 }
589 }
590
591 if (message.getGroupInfo().isPresent()) {
592 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
593 } else {
594 // Send to all individually, so sync messages are sent correctly
595 for (SignalServiceAddress address : recipientsTS) {
596 messageSender.sendMessage(address, message);
597 }
598 }
599
600 if (message.isEndSession()) {
601 for (SignalServiceAddress recipient : recipientsTS) {
602 handleEndSession(recipient.getNumber());
603 }
604 }
605 } finally {
606 save();
607 }
608 }
609
610 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) {
611 SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore);
612 try {
613 return cipher.decrypt(envelope);
614 } catch (Exception e) {
615 // TODO handle all exceptions
616 e.printStackTrace();
617 return null;
618 }
619 }
620
621 private void handleEndSession(String source) {
622 signalProtocolStore.deleteAllSessions(source);
623 }
624
625 public interface ReceiveMessageHandler {
626 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, GroupInfo group);
627 }
628
629 private GroupInfo handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) {
630 GroupInfo group = null;
631 if (message.getGroupInfo().isPresent()) {
632 SignalServiceGroup groupInfo = message.getGroupInfo().get();
633 switch (groupInfo.getType()) {
634 case UPDATE:
635 try {
636 group = groupStore.getGroup(groupInfo.getGroupId());
637 } catch (GroupNotFoundException e) {
638 group = new GroupInfo(groupInfo.getGroupId());
639 }
640
641 if (groupInfo.getAvatar().isPresent()) {
642 SignalServiceAttachment avatar = groupInfo.getAvatar().get();
643 if (avatar.isPointer()) {
644 long avatarId = avatar.asPointer().getId();
645 try {
646 retrieveAttachment(avatar.asPointer());
647 // TODO store group avatar in /avatar/groups folder
648 group.avatarId = avatarId;
649 } catch (IOException | InvalidMessageException e) {
650 System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage());
651 }
652 }
653 }
654
655 if (groupInfo.getName().isPresent()) {
656 group.name = groupInfo.getName().get();
657 }
658
659 if (groupInfo.getMembers().isPresent()) {
660 group.members.addAll(groupInfo.getMembers().get());
661 }
662
663 groupStore.updateGroup(group);
664 break;
665 case DELIVER:
666 try {
667 group = groupStore.getGroup(groupInfo.getGroupId());
668 } catch (GroupNotFoundException e) {
669 }
670 break;
671 case QUIT:
672 try {
673 group = groupStore.getGroup(groupInfo.getGroupId());
674 group.members.remove(source);
675 groupStore.updateGroup(group);
676 } catch (GroupNotFoundException e) {
677 }
678 break;
679 }
680 }
681 if (message.isEndSession()) {
682 handleEndSession(isSync ? destination : source);
683 }
684 if (message.getAttachments().isPresent()) {
685 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
686 if (attachment.isPointer()) {
687 try {
688 retrieveAttachment(attachment.asPointer());
689 } catch (IOException | InvalidMessageException e) {
690 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
691 }
692 }
693 }
694 }
695 return group;
696 }
697
698 public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
699 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
700 SignalServiceMessagePipe messagePipe = null;
701
702 try {
703 messagePipe = messageReceiver.createMessagePipe();
704
705 while (true) {
706 SignalServiceEnvelope envelope;
707 SignalServiceContent content = null;
708 GroupInfo group = null;
709 try {
710 envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS);
711 if (!envelope.isReceipt()) {
712 content = decryptMessage(envelope);
713 if (content != null) {
714 if (content.getDataMessage().isPresent()) {
715 SignalServiceDataMessage message = content.getDataMessage().get();
716 group = handleSignalServiceDataMessage(message, false, envelope.getSource(), username);
717 }
718 if (content.getSyncMessage().isPresent()) {
719 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
720 if (syncMessage.getSent().isPresent()) {
721 SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
722 group = handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get());
723 }
724 if (syncMessage.getRequest().isPresent()) {
725 RequestMessage rm = syncMessage.getRequest().get();
726 if (rm.isContactsRequest()) {
727 try {
728 sendContacts();
729 } catch (EncapsulatedExceptions encapsulatedExceptions) {
730 encapsulatedExceptions.printStackTrace();
731 } catch (UntrustedIdentityException e) {
732 e.printStackTrace();
733 }
734 }
735 if (rm.isGroupsRequest()) {
736 try {
737 sendGroups();
738 } catch (EncapsulatedExceptions encapsulatedExceptions) {
739 encapsulatedExceptions.printStackTrace();
740 } catch (UntrustedIdentityException e) {
741 e.printStackTrace();
742 }
743 }
744 }
745 if (syncMessage.getGroups().isPresent()) {
746 try {
747 DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer()));
748 DeviceGroup g;
749 while ((g = s.read()) != null) {
750 GroupInfo syncGroup;
751 try {
752 syncGroup = groupStore.getGroup(g.getId());
753 } catch (GroupNotFoundException e) {
754 syncGroup = new GroupInfo(g.getId());
755 }
756 if (g.getName().isPresent()) {
757 syncGroup.name = g.getName().get();
758 }
759 syncGroup.members.addAll(g.getMembers());
760 syncGroup.active = g.isActive();
761
762 if (g.getAvatar().isPresent()) {
763 byte[] ava = new byte[(int) g.getAvatar().get().getLength()];
764 org.whispersystems.signalservice.internal.util.Util.readFully(g.getAvatar().get().getInputStream(), ava);
765 // TODO store group avatar in /avatar/groups folder
766 }
767 groupStore.updateGroup(syncGroup);
768 }
769 } catch (Exception e) {
770 e.printStackTrace();
771 }
772 }
773 if (syncMessage.getContacts().isPresent()) {
774 try {
775 DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer()));
776 DeviceContact c;
777 while ((c = s.read()) != null) {
778 ContactInfo contact = new ContactInfo();
779 contact.number = c.getNumber();
780 if (c.getName().isPresent()) {
781 contact.name = c.getName().get();
782 }
783 contactStore.updateContact(contact);
784
785 if (c.getAvatar().isPresent()) {
786 byte[] ava = new byte[(int) c.getAvatar().get().getLength()];
787 org.whispersystems.signalservice.internal.util.Util.readFully(c.getAvatar().get().getInputStream(), ava);
788 // TODO store contact avatar in /avatar/contacts folder
789 }
790 }
791 } catch (Exception e) {
792 e.printStackTrace();
793 }
794 }
795 }
796 }
797 }
798 save();
799 handler.handleMessage(envelope, content, group);
800 } catch (TimeoutException e) {
801 if (returnOnTimeout)
802 return;
803 } catch (InvalidVersionException e) {
804 System.err.println("Ignoring error: " + e.getMessage());
805 }
806 }
807 } finally {
808 if (messagePipe != null)
809 messagePipe.shutdown();
810 }
811 }
812
813 public File getAttachmentFile(long attachmentId) {
814 return new File(attachmentsPath, attachmentId + "");
815 }
816
817 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
818 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
819
820 File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
821 InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
822
823 new File(attachmentsPath).mkdirs();
824 File outputFile = getAttachmentFile(pointer.getId());
825 OutputStream output = null;
826 try {
827 output = new FileOutputStream(outputFile);
828 byte[] buffer = new byte[4096];
829 int read;
830
831 while ((read = input.read(buffer)) != -1) {
832 output.write(buffer, 0, read);
833 }
834 } catch (FileNotFoundException e) {
835 e.printStackTrace();
836 return null;
837 } finally {
838 if (output != null) {
839 output.close();
840 output = null;
841 }
842 if (!tmpFile.delete()) {
843 System.err.println("Failed to delete temp file: " + tmpFile);
844 }
845 }
846 if (pointer.getPreview().isPresent()) {
847 File previewFile = new File(outputFile + ".preview");
848 try {
849 output = new FileOutputStream(previewFile);
850 byte[] preview = pointer.getPreview().get();
851 output.write(preview, 0, preview.length);
852 } catch (FileNotFoundException e) {
853 e.printStackTrace();
854 return null;
855 } finally {
856 if (output != null) {
857 output.close();
858 }
859 }
860 }
861 return outputFile;
862 }
863
864 private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
865 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
866 File file = File.createTempFile("ts_tmp", "tmp");
867 file.deleteOnExit();
868
869 return messageReceiver.retrieveAttachment(pointer, file);
870 }
871
872 private String canonicalizeNumber(String number) throws InvalidNumberException {
873 String localNumber = username;
874 return PhoneNumberFormatter.formatNumber(number, localNumber);
875 }
876
877 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
878 String e164number = canonicalizeNumber(number);
879 return new SignalServiceAddress(e164number);
880 }
881
882 @Override
883 public boolean isRemote() {
884 return false;
885 }
886
887 private void sendGroups() throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
888 File groupsFile = File.createTempFile("multidevice-group-update", ".tmp");
889
890 try {
891 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(groupsFile));
892 try {
893 for (GroupInfo record : groupStore.getGroups()) {
894 out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
895 new ArrayList<>(record.members), Optional.<SignalServiceAttachmentStream>absent(), // TODO
896 record.active));
897 }
898 } finally {
899 out.close();
900 }
901
902 if (groupsFile.exists() && groupsFile.length() > 0) {
903 FileInputStream contactsFileStream = new FileInputStream(groupsFile);
904 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
905 .withStream(contactsFileStream)
906 .withContentType("application/octet-stream")
907 .withLength(groupsFile.length())
908 .build();
909
910 sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
911 }
912 } finally {
913 groupsFile.delete();
914 }
915 }
916
917 private void sendContacts() throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
918 File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp");
919
920 try {
921 DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactsFile));
922 try {
923 for (ContactInfo record : contactStore.getContacts()) {
924 out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
925 Optional.<SignalServiceAttachmentStream>absent())); // TODO
926 }
927 } finally {
928 out.close();
929 }
930
931 if (contactsFile.exists() && contactsFile.length() > 0) {
932 FileInputStream contactsFileStream = new FileInputStream(contactsFile);
933 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
934 .withStream(contactsFileStream)
935 .withContentType("application/octet-stream")
936 .withLength(contactsFile.length())
937 .build();
938
939 sendMessage(SignalServiceSyncMessage.forContacts(attachmentStream));
940 }
941 } finally {
942 contactsFile.delete();
943 }
944 }
945 }