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