]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
Don’t remove self from group when sending group messages
[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
482 // Don't send group message to ourself
483 final List<String> membersSend = new ArrayList<>(g.members);
484 membersSend.remove(this.username);
485 sendMessage(message, membersSend);
486 }
487
488 public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException {
489 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
490 .withId(groupId)
491 .build();
492
493 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
494 .asGroupMessage(group)
495 .build();
496
497 final GroupInfo g = groupStore.getGroup(groupId);
498 if (g == null) {
499 throw new GroupNotFoundException(groupId);
500 }
501 g.members.remove(this.username);
502 groupStore.updateGroup(g);
503
504 sendMessage(message, g.members);
505 }
506
507 public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException {
508 GroupInfo g;
509 if (groupId == null) {
510 // Create new group
511 g = new GroupInfo(Util.getSecretBytes(16));
512 g.members.add(username);
513 } else {
514 g = groupStore.getGroup(groupId);
515 if (g == null) {
516 throw new GroupNotFoundException(groupId);
517 }
518 }
519
520 if (name != null) {
521 g.name = name;
522 }
523
524 if (members != null) {
525 for (String member : members) {
526 try {
527 g.members.add(canonicalizeNumber(member));
528 } catch (InvalidNumberException e) {
529 System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
530 System.err.println("Aborting…");
531 System.exit(1);
532 }
533 }
534 }
535
536 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
537 .withId(g.groupId)
538 .withName(g.name)
539 .withMembers(new ArrayList<>(g.members));
540
541 File aFile = getGroupAvatarFile(g.groupId);
542 if (avatarFile != null) {
543 new File(avatarsPath).mkdirs();
544 Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
545 }
546 if (aFile.exists()) {
547 try {
548 group.withAvatar(createAttachment(aFile));
549 } catch (IOException e) {
550 throw new AttachmentInvalidException(avatarFile, e);
551 }
552 }
553
554 groupStore.updateGroup(g);
555
556 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
557 .asGroupMessage(group.build())
558 .build();
559
560 // Don't send group message to ourself
561 final List<String> membersSend = new ArrayList<>(g.members);
562 membersSend.remove(this.username);
563 sendMessage(message, membersSend);
564 return g.groupId;
565 }
566
567 @Override
568 public void sendMessage(String message, List<String> attachments, String recipient)
569 throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException {
570 List<String> recipients = new ArrayList<>(1);
571 recipients.add(recipient);
572 sendMessage(message, attachments, recipients);
573 }
574
575 @Override
576 public void sendMessage(String messageText, List<String> attachments,
577 List<String> recipients)
578 throws IOException, EncapsulatedExceptions, AttachmentInvalidException, UntrustedIdentityException {
579 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
580 if (attachments != null) {
581 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
582 }
583 SignalServiceDataMessage message = messageBuilder.build();
584
585 sendMessage(message, recipients);
586 }
587
588 @Override
589 public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
590 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
591 .asEndSessionMessage()
592 .build();
593
594 sendMessage(message, recipients);
595 }
596
597 private void requestSyncGroups() throws IOException {
598 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
599 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
600 try {
601 sendMessage(message);
602 } catch (EncapsulatedExceptions encapsulatedExceptions) {
603 encapsulatedExceptions.printStackTrace();
604 } catch (UntrustedIdentityException e) {
605 e.printStackTrace();
606 }
607 }
608
609 private void requestSyncContacts() throws IOException {
610 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
611 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
612 try {
613 sendMessage(message);
614 } catch (EncapsulatedExceptions encapsulatedExceptions) {
615 encapsulatedExceptions.printStackTrace();
616 } catch (UntrustedIdentityException e) {
617 e.printStackTrace();
618 }
619 }
620
621 private void sendMessage(SignalServiceSyncMessage message)
622 throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
623 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
624 deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
625 messageSender.sendMessage(message);
626 }
627
628 private void sendMessage(SignalServiceDataMessage message, Collection<String> recipients)
629 throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
630 try {
631 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
632 deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
633
634 Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
635 for (String recipient : recipients) {
636 try {
637 recipientsTS.add(getPushAddress(recipient));
638 } catch (InvalidNumberException e) {
639 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
640 System.err.println("Aborting sending.");
641 save();
642 return;
643 }
644 }
645
646 if (message.getGroupInfo().isPresent()) {
647 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
648 } else {
649 // Send to all individually, so sync messages are sent correctly
650 for (SignalServiceAddress address : recipientsTS) {
651 messageSender.sendMessage(address, message);
652 }
653 }
654
655 if (message.isEndSession()) {
656 for (SignalServiceAddress recipient : recipientsTS) {
657 handleEndSession(recipient.getNumber());
658 }
659 }
660 } finally {
661 save();
662 }
663 }
664
665 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) {
666 SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore);
667 try {
668 return cipher.decrypt(envelope);
669 } catch (Exception e) {
670 // TODO handle all exceptions
671 e.printStackTrace();
672 return null;
673 }
674 }
675
676 private void handleEndSession(String source) {
677 signalProtocolStore.deleteAllSessions(source);
678 }
679
680 public interface ReceiveMessageHandler {
681 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent);
682 }
683
684 private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) {
685 if (message.getGroupInfo().isPresent()) {
686 SignalServiceGroup groupInfo = message.getGroupInfo().get();
687 switch (groupInfo.getType()) {
688 case UPDATE:
689 GroupInfo group;
690 group = groupStore.getGroup(groupInfo.getGroupId());
691 if (group == null) {
692 group = new GroupInfo(groupInfo.getGroupId());
693 }
694
695 if (groupInfo.getAvatar().isPresent()) {
696 SignalServiceAttachment avatar = groupInfo.getAvatar().get();
697 if (avatar.isPointer()) {
698 try {
699 retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId);
700 } catch (IOException | InvalidMessageException e) {
701 System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage());
702 }
703 }
704 }
705
706 if (groupInfo.getName().isPresent()) {
707 group.name = groupInfo.getName().get();
708 }
709
710 if (groupInfo.getMembers().isPresent()) {
711 group.members.addAll(groupInfo.getMembers().get());
712 }
713
714 groupStore.updateGroup(group);
715 break;
716 case DELIVER:
717 break;
718 case QUIT:
719 group = groupStore.getGroup(groupInfo.getGroupId());
720 if (group != null) {
721 group.members.remove(source);
722 groupStore.updateGroup(group);
723 }
724 break;
725 }
726 }
727 if (message.isEndSession()) {
728 handleEndSession(isSync ? destination : source);
729 }
730 if (message.getAttachments().isPresent()) {
731 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
732 if (attachment.isPointer()) {
733 try {
734 retrieveAttachment(attachment.asPointer());
735 } catch (IOException | InvalidMessageException e) {
736 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
737 }
738 }
739 }
740 }
741 }
742
743 public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
744 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
745 SignalServiceMessagePipe messagePipe = null;
746
747 try {
748 messagePipe = messageReceiver.createMessagePipe();
749
750 while (true) {
751 SignalServiceEnvelope envelope;
752 SignalServiceContent content = null;
753 try {
754 envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS);
755 if (!envelope.isReceipt()) {
756 content = decryptMessage(envelope);
757 if (content != null) {
758 if (content.getDataMessage().isPresent()) {
759 SignalServiceDataMessage message = content.getDataMessage().get();
760 handleSignalServiceDataMessage(message, false, envelope.getSource(), username);
761 }
762 if (content.getSyncMessage().isPresent()) {
763 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
764 if (syncMessage.getSent().isPresent()) {
765 SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
766 handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get());
767 }
768 if (syncMessage.getRequest().isPresent()) {
769 RequestMessage rm = syncMessage.getRequest().get();
770 if (rm.isContactsRequest()) {
771 try {
772 sendContacts();
773 } catch (EncapsulatedExceptions encapsulatedExceptions) {
774 encapsulatedExceptions.printStackTrace();
775 } catch (UntrustedIdentityException e) {
776 e.printStackTrace();
777 }
778 }
779 if (rm.isGroupsRequest()) {
780 try {
781 sendGroups();
782 } catch (EncapsulatedExceptions encapsulatedExceptions) {
783 encapsulatedExceptions.printStackTrace();
784 } catch (UntrustedIdentityException e) {
785 e.printStackTrace();
786 }
787 }
788 }
789 if (syncMessage.getGroups().isPresent()) {
790 try {
791 DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer()));
792 DeviceGroup g;
793 while ((g = s.read()) != null) {
794 GroupInfo syncGroup = groupStore.getGroup(g.getId());
795 if (syncGroup == null) {
796 syncGroup = new GroupInfo(g.getId());
797 }
798 if (g.getName().isPresent()) {
799 syncGroup.name = g.getName().get();
800 }
801 syncGroup.members.addAll(g.getMembers());
802 syncGroup.active = g.isActive();
803
804 if (g.getAvatar().isPresent()) {
805 retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
806 }
807 groupStore.updateGroup(syncGroup);
808 }
809 } catch (Exception e) {
810 e.printStackTrace();
811 }
812 }
813 if (syncMessage.getContacts().isPresent()) {
814 try {
815 DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer()));
816 DeviceContact c;
817 while ((c = s.read()) != null) {
818 ContactInfo contact = new ContactInfo();
819 contact.number = c.getNumber();
820 if (c.getName().isPresent()) {
821 contact.name = c.getName().get();
822 }
823 contactStore.updateContact(contact);
824
825 if (c.getAvatar().isPresent()) {
826 retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number);
827 }
828 }
829 } catch (Exception e) {
830 e.printStackTrace();
831 }
832 }
833 }
834 }
835 }
836 save();
837 handler.handleMessage(envelope, content);
838 } catch (TimeoutException e) {
839 if (returnOnTimeout)
840 return;
841 } catch (InvalidVersionException e) {
842 System.err.println("Ignoring error: " + e.getMessage());
843 }
844 }
845 } finally {
846 if (messagePipe != null)
847 messagePipe.shutdown();
848 }
849 }
850
851 public File getContactAvatarFile(String number) {
852 return new File(avatarsPath, "contact-" + number);
853 }
854
855 private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException {
856 new File(avatarsPath).mkdirs();
857 if (attachment.isPointer()) {
858 SignalServiceAttachmentPointer pointer = attachment.asPointer();
859 return retrieveAttachment(pointer, getContactAvatarFile(number), false);
860 } else {
861 SignalServiceAttachmentStream stream = attachment.asStream();
862 return retrieveAttachment(stream, getContactAvatarFile(number));
863 }
864 }
865
866 public File getGroupAvatarFile(byte[] groupId) {
867 return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
868 }
869
870 private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException {
871 new File(avatarsPath).mkdirs();
872 if (attachment.isPointer()) {
873 SignalServiceAttachmentPointer pointer = attachment.asPointer();
874 return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
875 } else {
876 SignalServiceAttachmentStream stream = attachment.asStream();
877 return retrieveAttachment(stream, getGroupAvatarFile(groupId));
878 }
879 }
880
881 public File getAttachmentFile(long attachmentId) {
882 return new File(attachmentsPath, attachmentId + "");
883 }
884
885 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
886 new File(attachmentsPath).mkdirs();
887 return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true);
888 }
889
890 private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException {
891 InputStream input = stream.getInputStream();
892
893 OutputStream output = null;
894 try {
895 output = new FileOutputStream(outputFile);
896 byte[] buffer = new byte[4096];
897 int read;
898
899 while ((read = input.read(buffer)) != -1) {
900 output.write(buffer, 0, read);
901 }
902 } catch (FileNotFoundException e) {
903 e.printStackTrace();
904 return null;
905 } finally {
906 if (output != null) {
907 output.close();
908 }
909 }
910 return outputFile;
911 }
912
913 private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException {
914 if (storePreview && pointer.getPreview().isPresent()) {
915 File previewFile = new File(outputFile + ".preview");
916 OutputStream output = null;
917 try {
918 output = new FileOutputStream(previewFile);
919 byte[] preview = pointer.getPreview().get();
920 output.write(preview, 0, preview.length);
921 } catch (FileNotFoundException e) {
922 e.printStackTrace();
923 return null;
924 } finally {
925 if (output != null) {
926 output.close();
927 }
928 }
929 }
930
931 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
932
933 File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
934 InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
935
936 OutputStream output = null;
937 try {
938 output = new FileOutputStream(outputFile);
939 byte[] buffer = new byte[4096];
940 int read;
941
942 while ((read = input.read(buffer)) != -1) {
943 output.write(buffer, 0, read);
944 }
945 } catch (FileNotFoundException e) {
946 e.printStackTrace();
947 return null;
948 } finally {
949 if (output != null) {
950 output.close();
951 }
952 if (!tmpFile.delete()) {
953 System.err.println("Failed to delete temp file: " + tmpFile);
954 }
955 }
956 return outputFile;
957 }
958
959 private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
960 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
961 File file = File.createTempFile("ts_tmp", "tmp");
962 file.deleteOnExit();
963
964 return messageReceiver.retrieveAttachment(pointer, file);
965 }
966
967 private String canonicalizeNumber(String number) throws InvalidNumberException {
968 String localNumber = username;
969 return PhoneNumberFormatter.formatNumber(number, localNumber);
970 }
971
972 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
973 String e164number = canonicalizeNumber(number);
974 return new SignalServiceAddress(e164number);
975 }
976
977 @Override
978 public boolean isRemote() {
979 return false;
980 }
981
982 private void sendGroups() throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
983 File groupsFile = File.createTempFile("multidevice-group-update", ".tmp");
984
985 try {
986 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(groupsFile));
987 try {
988 for (GroupInfo record : groupStore.getGroups()) {
989 out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
990 new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId),
991 record.active));
992 }
993 } finally {
994 out.close();
995 }
996
997 if (groupsFile.exists() && groupsFile.length() > 0) {
998 FileInputStream contactsFileStream = new FileInputStream(groupsFile);
999 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1000 .withStream(contactsFileStream)
1001 .withContentType("application/octet-stream")
1002 .withLength(groupsFile.length())
1003 .build();
1004
1005 sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
1006 }
1007 } finally {
1008 groupsFile.delete();
1009 }
1010 }
1011
1012 private void sendContacts() throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
1013 File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp");
1014
1015 try {
1016 DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactsFile));
1017 try {
1018 for (ContactInfo record : contactStore.getContacts()) {
1019 out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
1020 createContactAvatarAttachment(record.number)));
1021 }
1022 } finally {
1023 out.close();
1024 }
1025
1026 if (contactsFile.exists() && contactsFile.length() > 0) {
1027 FileInputStream contactsFileStream = new FileInputStream(contactsFile);
1028 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1029 .withStream(contactsFileStream)
1030 .withContentType("application/octet-stream")
1031 .withLength(contactsFile.length())
1032 .build();
1033
1034 sendMessage(SignalServiceSyncMessage.forContacts(attachmentStream));
1035 }
1036 } finally {
1037 contactsFile.delete();
1038 }
1039 }
1040
1041 public ContactInfo getContact(String number) {
1042 return contactStore.getContact(number);
1043 }
1044
1045 public GroupInfo getGroup(byte[] groupId) {
1046 return groupStore.getGroup(groupId);
1047 }
1048 }