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