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