]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
Update libsignal-service-java
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / ManagerImpl.java
1 /*
2 Copyright (C) 2015-2021 AsamK and contributors
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.manager;
18
19 import org.asamk.signal.manager.actions.HandleAction;
20 import org.asamk.signal.manager.api.Device;
21 import org.asamk.signal.manager.api.Group;
22 import org.asamk.signal.manager.api.Identity;
23 import org.asamk.signal.manager.api.InactiveGroupLinkException;
24 import org.asamk.signal.manager.api.InvalidDeviceLinkException;
25 import org.asamk.signal.manager.api.Message;
26 import org.asamk.signal.manager.api.Pair;
27 import org.asamk.signal.manager.api.RecipientIdentifier;
28 import org.asamk.signal.manager.api.SendGroupMessageResults;
29 import org.asamk.signal.manager.api.SendMessageResult;
30 import org.asamk.signal.manager.api.SendMessageResults;
31 import org.asamk.signal.manager.api.TypingAction;
32 import org.asamk.signal.manager.api.UpdateGroup;
33 import org.asamk.signal.manager.config.ServiceConfig;
34 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
35 import org.asamk.signal.manager.groups.GroupId;
36 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
37 import org.asamk.signal.manager.groups.GroupNotFoundException;
38 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
39 import org.asamk.signal.manager.groups.LastGroupAdminException;
40 import org.asamk.signal.manager.groups.NotAGroupMemberException;
41 import org.asamk.signal.manager.helper.AttachmentHelper;
42 import org.asamk.signal.manager.helper.ContactHelper;
43 import org.asamk.signal.manager.helper.GroupHelper;
44 import org.asamk.signal.manager.helper.GroupV2Helper;
45 import org.asamk.signal.manager.helper.IdentityHelper;
46 import org.asamk.signal.manager.helper.IncomingMessageHandler;
47 import org.asamk.signal.manager.helper.PinHelper;
48 import org.asamk.signal.manager.helper.PreKeyHelper;
49 import org.asamk.signal.manager.helper.ProfileHelper;
50 import org.asamk.signal.manager.helper.SendHelper;
51 import org.asamk.signal.manager.helper.StorageHelper;
52 import org.asamk.signal.manager.helper.SyncHelper;
53 import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
54 import org.asamk.signal.manager.jobs.Context;
55 import org.asamk.signal.manager.storage.SignalAccount;
56 import org.asamk.signal.manager.storage.groups.GroupInfo;
57 import org.asamk.signal.manager.storage.identities.IdentityInfo;
58 import org.asamk.signal.manager.storage.messageCache.CachedMessage;
59 import org.asamk.signal.manager.storage.recipients.Contact;
60 import org.asamk.signal.manager.storage.recipients.Profile;
61 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
62 import org.asamk.signal.manager.storage.recipients.RecipientId;
63 import org.asamk.signal.manager.storage.stickers.Sticker;
64 import org.asamk.signal.manager.storage.stickers.StickerPackId;
65 import org.asamk.signal.manager.util.KeyUtils;
66 import org.asamk.signal.manager.util.StickerUtils;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
69 import org.whispersystems.libsignal.InvalidKeyException;
70 import org.whispersystems.libsignal.ecc.ECPublicKey;
71 import org.whispersystems.libsignal.util.guava.Optional;
72 import org.whispersystems.signalservice.api.SignalSessionLock;
73 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
74 import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
75 import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
76 import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
77 import org.whispersystems.signalservice.api.push.ACI;
78 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
79 import org.whispersystems.signalservice.api.util.DeviceNameUtil;
80 import org.whispersystems.signalservice.api.util.InvalidNumberException;
81 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
82 import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
83 import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
84 import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
85 import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
86 import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
87 import org.whispersystems.signalservice.internal.util.Hex;
88 import org.whispersystems.signalservice.internal.util.Util;
89
90 import java.io.File;
91 import java.io.IOException;
92 import java.net.URI;
93 import java.net.URISyntaxException;
94 import java.net.URLEncoder;
95 import java.nio.charset.StandardCharsets;
96 import java.security.SignatureException;
97 import java.util.Collection;
98 import java.util.HashMap;
99 import java.util.HashSet;
100 import java.util.List;
101 import java.util.Map;
102 import java.util.Set;
103 import java.util.UUID;
104 import java.util.concurrent.ExecutorService;
105 import java.util.concurrent.Executors;
106 import java.util.concurrent.TimeUnit;
107 import java.util.concurrent.TimeoutException;
108 import java.util.concurrent.locks.ReentrantLock;
109 import java.util.stream.Collectors;
110
111 import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
112
113 public class ManagerImpl implements Manager {
114
115 private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class);
116
117 private final ServiceEnvironmentConfig serviceEnvironmentConfig;
118 private final SignalDependencies dependencies;
119
120 private SignalAccount account;
121
122 private final ExecutorService executor = Executors.newCachedThreadPool();
123
124 private final ProfileHelper profileHelper;
125 private final PinHelper pinHelper;
126 private final StorageHelper storageHelper;
127 private final SendHelper sendHelper;
128 private final SyncHelper syncHelper;
129 private final AttachmentHelper attachmentHelper;
130 private final GroupHelper groupHelper;
131 private final ContactHelper contactHelper;
132 private final IncomingMessageHandler incomingMessageHandler;
133 private final PreKeyHelper preKeyHelper;
134 private final IdentityHelper identityHelper;
135
136 private final Context context;
137 private boolean hasCaughtUpWithOldMessages = false;
138 private boolean ignoreAttachments = false;
139
140 private Thread receiveThread;
141 private final Set<ReceiveMessageHandler> messageHandlers = new HashSet<>();
142 private boolean isReceivingSynchronous;
143
144 ManagerImpl(
145 SignalAccount account,
146 PathConfig pathConfig,
147 ServiceEnvironmentConfig serviceEnvironmentConfig,
148 String userAgent
149 ) {
150 this.account = account;
151 this.serviceEnvironmentConfig = serviceEnvironmentConfig;
152
153 final var credentialsProvider = new DynamicCredentialsProvider(account.getAci(),
154 account.getUsername(),
155 account.getPassword(),
156 account.getDeviceId());
157 final var sessionLock = new SignalSessionLock() {
158 private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
159
160 @Override
161 public Lock acquire() {
162 LEGACY_LOCK.lock();
163 return LEGACY_LOCK::unlock;
164 }
165 };
166 this.dependencies = new SignalDependencies(serviceEnvironmentConfig,
167 userAgent,
168 credentialsProvider,
169 account.getSignalProtocolStore(),
170 executor,
171 sessionLock);
172 final var avatarStore = new AvatarStore(pathConfig.avatarsPath());
173 final var attachmentStore = new AttachmentStore(pathConfig.attachmentsPath());
174 final var stickerPackStore = new StickerPackStore(pathConfig.stickerPacksPath());
175
176 this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore);
177 this.pinHelper = new PinHelper(dependencies.getKeyBackupService());
178 final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account,
179 dependencies,
180 account::getProfileKey,
181 this::getRecipientProfile);
182 this.profileHelper = new ProfileHelper(account,
183 dependencies,
184 avatarStore,
185 unidentifiedAccessHelper::getAccessFor,
186 this::resolveSignalServiceAddress);
187 final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential,
188 this::getRecipientProfile,
189 account::getSelfRecipientId,
190 dependencies.getGroupsV2Operations(),
191 dependencies.getGroupsV2Api(),
192 this::resolveSignalServiceAddress);
193 this.sendHelper = new SendHelper(account,
194 dependencies,
195 unidentifiedAccessHelper,
196 this::resolveSignalServiceAddress,
197 account.getRecipientStore(),
198 this::handleIdentityFailure,
199 this::getGroupInfo,
200 this::refreshRegisteredUser);
201 this.groupHelper = new GroupHelper(account,
202 dependencies,
203 attachmentHelper,
204 sendHelper,
205 groupV2Helper,
206 avatarStore,
207 this::resolveSignalServiceAddress,
208 account.getRecipientStore());
209 this.storageHelper = new StorageHelper(account, dependencies, groupHelper, profileHelper);
210 this.contactHelper = new ContactHelper(account);
211 this.syncHelper = new SyncHelper(account,
212 attachmentHelper,
213 sendHelper,
214 groupHelper,
215 avatarStore,
216 this::resolveSignalServiceAddress);
217 preKeyHelper = new PreKeyHelper(account, dependencies);
218
219 this.context = new Context(account,
220 dependencies,
221 stickerPackStore,
222 sendHelper,
223 groupHelper,
224 syncHelper,
225 profileHelper,
226 storageHelper,
227 preKeyHelper);
228 var jobExecutor = new JobExecutor(context);
229
230 this.incomingMessageHandler = new IncomingMessageHandler(account,
231 dependencies,
232 account.getRecipientStore(),
233 this::resolveSignalServiceAddress,
234 groupHelper,
235 contactHelper,
236 attachmentHelper,
237 syncHelper,
238 this::getRecipientProfile,
239 jobExecutor);
240 this.identityHelper = new IdentityHelper(account,
241 dependencies,
242 this::resolveSignalServiceAddress,
243 syncHelper,
244 profileHelper);
245 }
246
247 @Override
248 public String getSelfNumber() {
249 return account.getUsername();
250 }
251
252 @Override
253 public void checkAccountState() throws IOException {
254 if (account.getLastReceiveTimestamp() == 0) {
255 logger.info("The Signal protocol expects that incoming messages are regularly received.");
256 } else {
257 var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp();
258 long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS);
259 if (days > 7) {
260 logger.warn(
261 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
262 days);
263 }
264 }
265 preKeyHelper.refreshPreKeysIfNecessary();
266 if (account.getAci() == null) {
267 account.setAci(dependencies.getAccountManager().getOwnAci());
268 }
269 updateAccountAttributes(null);
270 }
271
272 /**
273 * This is used for checking a set of phone numbers for registration on Signal
274 *
275 * @param numbers The set of phone number in question
276 * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null.
277 * @throws IOException if its unable to get the contacts to check if they're registered
278 */
279 @Override
280 public Map<String, Pair<String, UUID>> areUsersRegistered(Set<String> numbers) throws IOException {
281 Map<String, String> canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> {
282 try {
283 return PhoneNumberFormatter.formatNumber(n, account.getUsername());
284 } catch (InvalidNumberException e) {
285 return "";
286 }
287 }));
288
289 // Note "registeredUsers" has no optionals. It only gives us info on users who are registered
290 var registeredUsers = getRegisteredUsers(canonicalizedNumbers.values()
291 .stream()
292 .filter(s -> !s.isEmpty())
293 .collect(Collectors.toSet()));
294
295 return numbers.stream().collect(Collectors.toMap(n -> n, n -> {
296 final var number = canonicalizedNumbers.get(n);
297 final var aci = registeredUsers.get(number);
298 return new Pair<>(number.isEmpty() ? null : number, aci == null ? null : aci.uuid());
299 }));
300 }
301
302 @Override
303 public void updateAccountAttributes(String deviceName) throws IOException {
304 final String encryptedDeviceName;
305 if (deviceName == null) {
306 encryptedDeviceName = account.getEncryptedDeviceName();
307 } else {
308 final var privateKey = account.getIdentityKeyPair().getPrivateKey();
309 encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
310 account.setEncryptedDeviceName(encryptedDeviceName);
311 }
312 dependencies.getAccountManager()
313 .setAccountAttributes(encryptedDeviceName,
314 null,
315 account.getLocalRegistrationId(),
316 true,
317 null,
318 account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
319 account.getSelfUnidentifiedAccessKey(),
320 account.isUnrestrictedUnidentifiedAccess(),
321 capabilities,
322 account.isDiscoverableByPhoneNumber());
323 }
324
325 @Override
326 public void updateConfiguration(
327 final Boolean readReceipts,
328 final Boolean unidentifiedDeliveryIndicators,
329 final Boolean typingIndicators,
330 final Boolean linkPreviews
331 ) throws IOException, NotMasterDeviceException {
332 if (!account.isMasterDevice()) {
333 throw new NotMasterDeviceException();
334 }
335
336 final var configurationStore = account.getConfigurationStore();
337 if (readReceipts != null) {
338 configurationStore.setReadReceipts(readReceipts);
339 }
340 if (unidentifiedDeliveryIndicators != null) {
341 configurationStore.setUnidentifiedDeliveryIndicators(unidentifiedDeliveryIndicators);
342 }
343 if (typingIndicators != null) {
344 configurationStore.setTypingIndicators(typingIndicators);
345 }
346 if (linkPreviews != null) {
347 configurationStore.setLinkPreviews(linkPreviews);
348 }
349 syncHelper.sendConfigurationMessage();
350 }
351
352 /**
353 * @param givenName if null, the previous givenName will be kept
354 * @param familyName if null, the previous familyName will be kept
355 * @param about if null, the previous about text will be kept
356 * @param aboutEmoji if null, the previous about emoji will be kept
357 * @param avatar if avatar is null the image from the local avatar store is used (if present),
358 */
359 @Override
360 public void setProfile(
361 String givenName, final String familyName, String about, String aboutEmoji, java.util.Optional<File> avatar
362 ) throws IOException {
363 profileHelper.setProfile(givenName,
364 familyName,
365 about,
366 aboutEmoji,
367 avatar == null ? null : Optional.fromNullable(avatar.orElse(null)));
368 syncHelper.sendSyncFetchProfileMessage();
369 }
370
371 @Override
372 public void unregister() throws IOException {
373 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
374 // If this is the master device, other users can't send messages to this number anymore.
375 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
376 dependencies.getAccountManager().setGcmId(Optional.absent());
377
378 account.setRegistered(false);
379 }
380
381 @Override
382 public void deleteAccount() throws IOException {
383 try {
384 pinHelper.removeRegistrationLockPin();
385 } catch (IOException e) {
386 logger.warn("Failed to remove registration lock pin");
387 }
388 account.setRegistrationLockPin(null, null);
389
390 dependencies.getAccountManager().deleteAccount();
391
392 account.setRegistered(false);
393 }
394
395 @Override
396 public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException {
397 dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
398 }
399
400 @Override
401 public List<Device> getLinkedDevices() throws IOException {
402 var devices = dependencies.getAccountManager().getDevices();
403 account.setMultiDevice(devices.size() > 1);
404 var identityKey = account.getIdentityKeyPair().getPrivateKey();
405 return devices.stream().map(d -> {
406 String deviceName = d.getName();
407 if (deviceName != null) {
408 try {
409 deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey);
410 } catch (IOException e) {
411 logger.debug("Failed to decrypt device name, maybe plain text?", e);
412 }
413 }
414 return new Device(d.getId(),
415 deviceName,
416 d.getCreated(),
417 d.getLastSeen(),
418 d.getId() == account.getDeviceId());
419 }).collect(Collectors.toList());
420 }
421
422 @Override
423 public void removeLinkedDevices(long deviceId) throws IOException {
424 dependencies.getAccountManager().removeDevice(deviceId);
425 var devices = dependencies.getAccountManager().getDevices();
426 account.setMultiDevice(devices.size() > 1);
427 }
428
429 @Override
430 public void addDeviceLink(URI linkUri) throws IOException, InvalidDeviceLinkException {
431 var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri);
432
433 addDevice(info.deviceIdentifier(), info.deviceKey());
434 }
435
436 private void addDevice(
437 String deviceIdentifier, ECPublicKey deviceKey
438 ) throws IOException, InvalidDeviceLinkException {
439 var identityKeyPair = account.getIdentityKeyPair();
440 var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
441
442 try {
443 dependencies.getAccountManager()
444 .addDevice(deviceIdentifier,
445 deviceKey,
446 identityKeyPair,
447 Optional.of(account.getProfileKey().serialize()),
448 verificationCode);
449 } catch (InvalidKeyException e) {
450 throw new InvalidDeviceLinkException("Invalid device link", e);
451 }
452 account.setMultiDevice(true);
453 }
454
455 @Override
456 public void setRegistrationLockPin(java.util.Optional<String> pin) throws IOException {
457 if (!account.isMasterDevice()) {
458 throw new RuntimeException("Only master device can set a PIN");
459 }
460 if (pin.isPresent()) {
461 final var masterKey = account.getPinMasterKey() != null
462 ? account.getPinMasterKey()
463 : KeyUtils.createMasterKey();
464
465 pinHelper.setRegistrationLockPin(pin.get(), masterKey);
466
467 account.setRegistrationLockPin(pin.get(), masterKey);
468 } else {
469 // Remove KBS Pin
470 pinHelper.removeRegistrationLockPin();
471
472 account.setRegistrationLockPin(null, null);
473 }
474 }
475
476 void refreshPreKeys() throws IOException {
477 preKeyHelper.refreshPreKeys();
478 }
479
480 @Override
481 public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws IOException {
482 return profileHelper.getRecipientProfile(resolveRecipient(recipient));
483 }
484
485 private Profile getRecipientProfile(RecipientId recipientId) {
486 return profileHelper.getRecipientProfile(recipientId);
487 }
488
489 @Override
490 public List<Group> getGroups() {
491 return account.getGroupStore().getGroups().stream().map(this::toGroup).collect(Collectors.toList());
492 }
493
494 private Group toGroup(final GroupInfo groupInfo) {
495 if (groupInfo == null) {
496 return null;
497 }
498
499 return new Group(groupInfo.getGroupId(),
500 groupInfo.getTitle(),
501 groupInfo.getDescription(),
502 groupInfo.getGroupInviteLink(),
503 groupInfo.getMembers()
504 .stream()
505 .map(account.getRecipientStore()::resolveRecipientAddress)
506 .collect(Collectors.toSet()),
507 groupInfo.getPendingMembers()
508 .stream()
509 .map(account.getRecipientStore()::resolveRecipientAddress)
510 .collect(Collectors.toSet()),
511 groupInfo.getRequestingMembers()
512 .stream()
513 .map(account.getRecipientStore()::resolveRecipientAddress)
514 .collect(Collectors.toSet()),
515 groupInfo.getAdminMembers()
516 .stream()
517 .map(account.getRecipientStore()::resolveRecipientAddress)
518 .collect(Collectors.toSet()),
519 groupInfo.isBlocked(),
520 groupInfo.getMessageExpirationTimer(),
521 groupInfo.getPermissionAddMember(),
522 groupInfo.getPermissionEditDetails(),
523 groupInfo.getPermissionSendMessage(),
524 groupInfo.isMember(account.getSelfRecipientId()),
525 groupInfo.isAdmin(account.getSelfRecipientId()));
526 }
527
528 @Override
529 public SendGroupMessageResults quitGroup(
530 GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins
531 ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException {
532 final var newAdmins = resolveRecipients(groupAdmins);
533 return groupHelper.quitGroup(groupId, newAdmins);
534 }
535
536 @Override
537 public void deleteGroup(GroupId groupId) throws IOException {
538 groupHelper.deleteGroup(groupId);
539 }
540
541 @Override
542 public Pair<GroupId, SendGroupMessageResults> createGroup(
543 String name, Set<RecipientIdentifier.Single> members, File avatarFile
544 ) throws IOException, AttachmentInvalidException {
545 return groupHelper.createGroup(name, members == null ? null : resolveRecipients(members), avatarFile);
546 }
547
548 @Override
549 public SendGroupMessageResults updateGroup(
550 final GroupId groupId, final UpdateGroup updateGroup
551 ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
552 return groupHelper.updateGroup(groupId,
553 updateGroup.getName(),
554 updateGroup.getDescription(),
555 updateGroup.getMembers() == null ? null : resolveRecipients(updateGroup.getMembers()),
556 updateGroup.getRemoveMembers() == null ? null : resolveRecipients(updateGroup.getRemoveMembers()),
557 updateGroup.getAdmins() == null ? null : resolveRecipients(updateGroup.getAdmins()),
558 updateGroup.getRemoveAdmins() == null ? null : resolveRecipients(updateGroup.getRemoveAdmins()),
559 updateGroup.isResetGroupLink(),
560 updateGroup.getGroupLinkState(),
561 updateGroup.getAddMemberPermission(),
562 updateGroup.getEditDetailsPermission(),
563 updateGroup.getAvatarFile(),
564 updateGroup.getExpirationTimer(),
565 updateGroup.getIsAnnouncementGroup());
566 }
567
568 @Override
569 public Pair<GroupId, SendGroupMessageResults> joinGroup(
570 GroupInviteLinkUrl inviteLinkUrl
571 ) throws IOException, InactiveGroupLinkException {
572 return groupHelper.joinGroup(inviteLinkUrl);
573 }
574
575 private SendMessageResults sendMessage(
576 SignalServiceDataMessage.Builder messageBuilder, Set<RecipientIdentifier> recipients
577 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
578 var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
579 long timestamp = System.currentTimeMillis();
580 messageBuilder.withTimestamp(timestamp);
581 for (final var recipient : recipients) {
582 if (recipient instanceof RecipientIdentifier.Single single) {
583 final var recipientId = resolveRecipient(single);
584 final var result = sendHelper.sendMessage(messageBuilder, recipientId);
585 results.put(recipient,
586 List.of(SendMessageResult.from(result,
587 account.getRecipientStore(),
588 account.getRecipientStore()::resolveRecipientAddress)));
589 } else if (recipient instanceof RecipientIdentifier.NoteToSelf) {
590 final var result = sendHelper.sendSelfMessage(messageBuilder);
591 results.put(recipient,
592 List.of(SendMessageResult.from(result,
593 account.getRecipientStore(),
594 account.getRecipientStore()::resolveRecipientAddress)));
595 } else if (recipient instanceof RecipientIdentifier.Group group) {
596 final var result = sendHelper.sendAsGroupMessage(messageBuilder, group.groupId());
597 results.put(recipient,
598 result.stream()
599 .map(sendMessageResult -> SendMessageResult.from(sendMessageResult,
600 account.getRecipientStore(),
601 account.getRecipientStore()::resolveRecipientAddress))
602 .collect(Collectors.toList()));
603 }
604 }
605 return new SendMessageResults(timestamp, results);
606 }
607
608 private void sendTypingMessage(
609 SignalServiceTypingMessage.Action action, Set<RecipientIdentifier> recipients
610 ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
611 final var timestamp = System.currentTimeMillis();
612 for (var recipient : recipients) {
613 if (recipient instanceof RecipientIdentifier.Single) {
614 final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent());
615 final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient);
616 sendHelper.sendTypingMessage(message, recipientId);
617 } else if (recipient instanceof RecipientIdentifier.Group) {
618 final var groupId = ((RecipientIdentifier.Group) recipient).groupId();
619 final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize()));
620 sendHelper.sendGroupTypingMessage(message, groupId);
621 }
622 }
623 }
624
625 @Override
626 public void sendTypingMessage(
627 TypingAction action, Set<RecipientIdentifier> recipients
628 ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
629 sendTypingMessage(action.toSignalService(), recipients);
630 }
631
632 @Override
633 public void sendReadReceipt(
634 RecipientIdentifier.Single sender, List<Long> messageIds
635 ) throws IOException, UntrustedIdentityException {
636 var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
637 messageIds,
638 System.currentTimeMillis());
639
640 sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender));
641 }
642
643 @Override
644 public void sendViewedReceipt(
645 RecipientIdentifier.Single sender, List<Long> messageIds
646 ) throws IOException, UntrustedIdentityException {
647 var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
648 messageIds,
649 System.currentTimeMillis());
650
651 sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender));
652 }
653
654 @Override
655 public SendMessageResults sendMessage(
656 Message message, Set<RecipientIdentifier> recipients
657 ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
658 final var messageBuilder = SignalServiceDataMessage.newBuilder();
659 applyMessage(messageBuilder, message);
660 return sendMessage(messageBuilder, recipients);
661 }
662
663 private void applyMessage(
664 final SignalServiceDataMessage.Builder messageBuilder, final Message message
665 ) throws AttachmentInvalidException, IOException {
666 messageBuilder.withBody(message.messageText());
667 final var attachments = message.attachments();
668 if (attachments != null) {
669 messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments));
670 }
671 }
672
673 @Override
674 public SendMessageResults sendRemoteDeleteMessage(
675 long targetSentTimestamp, Set<RecipientIdentifier> recipients
676 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
677 var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
678 final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
679 return sendMessage(messageBuilder, recipients);
680 }
681
682 @Override
683 public SendMessageResults sendMessageReaction(
684 String emoji,
685 boolean remove,
686 RecipientIdentifier.Single targetAuthor,
687 long targetSentTimestamp,
688 Set<RecipientIdentifier> recipients
689 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
690 var targetAuthorRecipientId = resolveRecipient(targetAuthor);
691 var reaction = new SignalServiceDataMessage.Reaction(emoji,
692 remove,
693 resolveSignalServiceAddress(targetAuthorRecipientId),
694 targetSentTimestamp);
695 final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction);
696 return sendMessage(messageBuilder, recipients);
697 }
698
699 @Override
700 public SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException {
701 var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
702
703 try {
704 return sendMessage(messageBuilder,
705 recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()));
706 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
707 throw new AssertionError(e);
708 } finally {
709 for (var recipient : recipients) {
710 final var recipientId = resolveRecipient(recipient);
711 account.getSessionStore().deleteAllSessions(recipientId);
712 }
713 }
714 }
715
716 @Override
717 public void setContactName(
718 RecipientIdentifier.Single recipient, String name
719 ) throws NotMasterDeviceException, IOException {
720 if (!account.isMasterDevice()) {
721 throw new NotMasterDeviceException();
722 }
723 contactHelper.setContactName(resolveRecipient(recipient), name);
724 }
725
726 @Override
727 public void setContactBlocked(
728 RecipientIdentifier.Single recipient, boolean blocked
729 ) throws NotMasterDeviceException, IOException {
730 if (!account.isMasterDevice()) {
731 throw new NotMasterDeviceException();
732 }
733 contactHelper.setContactBlocked(resolveRecipient(recipient), blocked);
734 // TODO cycle our profile key
735 syncHelper.sendBlockedList();
736 }
737
738 @Override
739 public void setGroupBlocked(
740 final GroupId groupId, final boolean blocked
741 ) throws GroupNotFoundException, IOException, NotMasterDeviceException {
742 if (!account.isMasterDevice()) {
743 throw new NotMasterDeviceException();
744 }
745 groupHelper.setGroupBlocked(groupId, blocked);
746 // TODO cycle our profile key
747 syncHelper.sendBlockedList();
748 }
749
750 /**
751 * Change the expiration timer for a contact
752 */
753 @Override
754 public void setExpirationTimer(
755 RecipientIdentifier.Single recipient, int messageExpirationTimer
756 ) throws IOException {
757 var recipientId = resolveRecipient(recipient);
758 contactHelper.setExpirationTimer(recipientId, messageExpirationTimer);
759 final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
760 try {
761 sendMessage(messageBuilder, Set.of(recipient));
762 } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) {
763 throw new AssertionError(e);
764 }
765 }
766
767 /**
768 * Upload the sticker pack from path.
769 *
770 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
771 * @return if successful, returns the URL to install the sticker pack in the signal app
772 */
773 @Override
774 public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException {
775 var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path);
776
777 var messageSender = dependencies.getMessageSender();
778
779 var packKey = KeyUtils.createStickerUploadKey();
780 var packIdString = messageSender.uploadStickerManifest(manifest, packKey);
781 var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString));
782
783 var sticker = new Sticker(packId, packKey);
784 account.getStickerStore().updateSticker(sticker);
785
786 try {
787 return new URI("https",
788 "signal.art",
789 "/addstickers/",
790 "pack_id="
791 + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8)
792 + "&pack_key="
793 + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8));
794 } catch (URISyntaxException e) {
795 throw new AssertionError(e);
796 }
797 }
798
799 @Override
800 public void requestAllSyncData() throws IOException {
801 syncHelper.requestAllSyncData();
802 retrieveRemoteStorage();
803 }
804
805 void retrieveRemoteStorage() throws IOException {
806 if (account.getStorageKey() != null) {
807 storageHelper.readDataFromStorage();
808 }
809 }
810
811 private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException {
812 final var address = resolveSignalServiceAddress(recipientId);
813 if (!address.getNumber().isPresent()) {
814 return recipientId;
815 }
816 final var number = address.getNumber().get();
817 final var uuid = getRegisteredUser(number);
818 return resolveRecipientTrusted(new SignalServiceAddress(uuid, number));
819 }
820
821 private ACI getRegisteredUser(final String number) throws IOException {
822 final Map<String, ACI> aciMap;
823 try {
824 aciMap = getRegisteredUsers(Set.of(number));
825 } catch (NumberFormatException e) {
826 throw new IOException(number, e);
827 }
828 final var uuid = aciMap.get(number);
829 if (uuid == null) {
830 throw new IOException(number, null);
831 }
832 return uuid;
833 }
834
835 private Map<String, ACI> getRegisteredUsers(final Set<String> numbers) throws IOException {
836 final Map<String, ACI> registeredUsers;
837 try {
838 registeredUsers = dependencies.getAccountManager()
839 .getRegisteredUsers(ServiceConfig.getIasKeyStore(),
840 numbers,
841 serviceEnvironmentConfig.getCdsMrenclave());
842 } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) {
843 throw new IOException(e);
844 }
845
846 // Store numbers as recipients, so we have the number/uuid association
847 registeredUsers.forEach((number, aci) -> resolveRecipientTrusted(new SignalServiceAddress(aci, number)));
848
849 return registeredUsers;
850 }
851
852 private void retryFailedReceivedMessages(ReceiveMessageHandler handler) {
853 Set<HandleAction> queuedActions = new HashSet<>();
854 for (var cachedMessage : account.getMessageCache().getCachedMessages()) {
855 var actions = retryFailedReceivedMessage(handler, cachedMessage);
856 if (actions != null) {
857 queuedActions.addAll(actions);
858 }
859 }
860 handleQueuedActions(queuedActions);
861 }
862
863 private List<HandleAction> retryFailedReceivedMessage(
864 final ReceiveMessageHandler handler, final CachedMessage cachedMessage
865 ) {
866 var envelope = cachedMessage.loadEnvelope();
867 if (envelope == null) {
868 cachedMessage.delete();
869 return null;
870 }
871
872 final var result = incomingMessageHandler.handleRetryEnvelope(envelope, ignoreAttachments, handler);
873 final var actions = result.first();
874 final var exception = result.second();
875
876 if (exception instanceof UntrustedIdentityException) {
877 if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) {
878 // Envelope is more than a month old, cleaning up.
879 cachedMessage.delete();
880 return null;
881 }
882 if (!envelope.hasSourceUuid()) {
883 final var identifier = ((UntrustedIdentityException) exception).getSender();
884 final var recipientId = account.getRecipientStore().resolveRecipient(identifier);
885 try {
886 account.getMessageCache().replaceSender(cachedMessage, recipientId);
887 } catch (IOException ioException) {
888 logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage());
889 }
890 }
891 return null;
892 }
893
894 // If successful and for all other errors that are not recoverable, delete the cached message
895 cachedMessage.delete();
896 return actions;
897 }
898
899 @Override
900 public void addReceiveHandler(final ReceiveMessageHandler handler) {
901 if (isReceivingSynchronous) {
902 throw new IllegalStateException("Already receiving message synchronously.");
903 }
904 synchronized (messageHandlers) {
905 messageHandlers.add(handler);
906
907 startReceiveThreadIfRequired();
908 }
909 }
910
911 private void startReceiveThreadIfRequired() {
912 if (receiveThread != null) {
913 return;
914 }
915 receiveThread = new Thread(() -> {
916 while (!Thread.interrupted()) {
917 try {
918 receiveMessagesInternal(1L, TimeUnit.HOURS, false, (envelope, e) -> {
919 synchronized (messageHandlers) {
920 for (ReceiveMessageHandler h : messageHandlers) {
921 try {
922 h.handleMessage(envelope, e);
923 } catch (Exception ex) {
924 logger.warn("Message handler failed, ignoring", ex);
925 }
926 }
927 }
928 });
929 break;
930 } catch (IOException e) {
931 logger.warn("Receiving messages failed, retrying", e);
932 }
933 }
934 hasCaughtUpWithOldMessages = false;
935 synchronized (messageHandlers) {
936 receiveThread = null;
937
938 // Check if in the meantime another handler has been registered
939 if (!messageHandlers.isEmpty()) {
940 startReceiveThreadIfRequired();
941 }
942 }
943 });
944
945 receiveThread.start();
946 }
947
948 @Override
949 public void removeReceiveHandler(final ReceiveMessageHandler handler) {
950 final Thread thread;
951 synchronized (messageHandlers) {
952 thread = receiveThread;
953 receiveThread = null;
954 messageHandlers.remove(handler);
955 if (!messageHandlers.isEmpty() || isReceivingSynchronous) {
956 return;
957 }
958 }
959
960 stopReceiveThread(thread);
961 }
962
963 private void stopReceiveThread(final Thread thread) {
964 thread.interrupt();
965 try {
966 thread.join();
967 } catch (InterruptedException ignored) {
968 }
969 }
970
971 @Override
972 public boolean isReceiving() {
973 if (isReceivingSynchronous) {
974 return true;
975 }
976 synchronized (messageHandlers) {
977 return messageHandlers.size() > 0;
978 }
979 }
980
981 @Override
982 public void receiveMessages(long timeout, TimeUnit unit, ReceiveMessageHandler handler) throws IOException {
983 receiveMessages(timeout, unit, true, handler);
984 }
985
986 @Override
987 public void receiveMessages(ReceiveMessageHandler handler) throws IOException {
988 receiveMessages(1L, TimeUnit.HOURS, false, handler);
989 }
990
991 private void receiveMessages(
992 long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler
993 ) throws IOException {
994 if (isReceiving()) {
995 throw new IllegalStateException("Already receiving message.");
996 }
997 isReceivingSynchronous = true;
998 receiveThread = Thread.currentThread();
999 try {
1000 receiveMessagesInternal(timeout, unit, returnOnTimeout, handler);
1001 } finally {
1002 receiveThread = null;
1003 hasCaughtUpWithOldMessages = false;
1004 isReceivingSynchronous = false;
1005 }
1006 }
1007
1008 private void receiveMessagesInternal(
1009 long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler
1010 ) throws IOException {
1011 retryFailedReceivedMessages(handler);
1012
1013 Set<HandleAction> queuedActions = new HashSet<>();
1014
1015 final var signalWebSocket = dependencies.getSignalWebSocket();
1016 signalWebSocket.connect();
1017
1018 hasCaughtUpWithOldMessages = false;
1019 var backOffCounter = 0;
1020 final var MAX_BACKOFF_COUNTER = 9;
1021
1022 while (!Thread.interrupted()) {
1023 SignalServiceEnvelope envelope;
1024 final CachedMessage[] cachedMessage = {null};
1025 account.setLastReceiveTimestamp(System.currentTimeMillis());
1026 logger.debug("Checking for new message from server");
1027 try {
1028 var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> {
1029 final var recipientId = envelope1.hasSourceUuid()
1030 ? resolveRecipient(envelope1.getSourceAddress())
1031 : null;
1032 // store message on disk, before acknowledging receipt to the server
1033 cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
1034 });
1035 backOffCounter = 0;
1036
1037 if (result.isPresent()) {
1038 envelope = result.get();
1039 logger.debug("New message received from server");
1040 } else {
1041 logger.debug("Received indicator that server queue is empty");
1042 handleQueuedActions(queuedActions);
1043 queuedActions.clear();
1044
1045 hasCaughtUpWithOldMessages = true;
1046 synchronized (this) {
1047 this.notifyAll();
1048 }
1049
1050 // Continue to wait another timeout for new messages
1051 continue;
1052 }
1053 } catch (AssertionError e) {
1054 if (e.getCause() instanceof InterruptedException) {
1055 Thread.currentThread().interrupt();
1056 break;
1057 } else {
1058 throw e;
1059 }
1060 } catch (IOException e) {
1061 logger.debug("Pipe unexpectedly unavailable: {}", e.getMessage());
1062 if (e instanceof WebSocketUnavailableException || "Connection closed!".equals(e.getMessage())) {
1063 final var sleepMilliseconds = 100 * (long) Math.pow(2, backOffCounter);
1064 backOffCounter = Math.min(backOffCounter + 1, MAX_BACKOFF_COUNTER);
1065 logger.warn("Connection closed unexpectedly, reconnecting in {} ms", sleepMilliseconds);
1066 try {
1067 Thread.sleep(sleepMilliseconds);
1068 } catch (InterruptedException interruptedException) {
1069 return;
1070 }
1071 hasCaughtUpWithOldMessages = false;
1072 signalWebSocket.connect();
1073 continue;
1074 }
1075 throw e;
1076 } catch (TimeoutException e) {
1077 backOffCounter = 0;
1078 if (returnOnTimeout) return;
1079 continue;
1080 }
1081
1082 final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler);
1083 queuedActions.addAll(result.first());
1084 final var exception = result.second();
1085
1086 if (hasCaughtUpWithOldMessages) {
1087 handleQueuedActions(queuedActions);
1088 queuedActions.clear();
1089 }
1090 if (cachedMessage[0] != null) {
1091 if (exception instanceof UntrustedIdentityException) {
1092 logger.debug("Keeping message with untrusted identity in message cache");
1093 final var address = ((UntrustedIdentityException) exception).getSender();
1094 final var recipientId = resolveRecipient(address);
1095 if (!envelope.hasSourceUuid()) {
1096 try {
1097 cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId);
1098 } catch (IOException ioException) {
1099 logger.warn("Failed to move cached message to recipient folder: {}",
1100 ioException.getMessage());
1101 }
1102 }
1103 } else {
1104 cachedMessage[0].delete();
1105 }
1106 }
1107 }
1108 handleQueuedActions(queuedActions);
1109 queuedActions.clear();
1110 }
1111
1112 @Override
1113 public void setIgnoreAttachments(final boolean ignoreAttachments) {
1114 this.ignoreAttachments = ignoreAttachments;
1115 }
1116
1117 @Override
1118 public boolean hasCaughtUpWithOldMessages() {
1119 return hasCaughtUpWithOldMessages;
1120 }
1121
1122 private void handleQueuedActions(final Collection<HandleAction> queuedActions) {
1123 logger.debug("Handling message actions");
1124 var interrupted = false;
1125 for (var action : queuedActions) {
1126 try {
1127 action.execute(context);
1128 } catch (Throwable e) {
1129 if ((e instanceof AssertionError || e instanceof RuntimeException)
1130 && e.getCause() instanceof InterruptedException) {
1131 interrupted = true;
1132 continue;
1133 }
1134 logger.warn("Message action failed.", e);
1135 }
1136 }
1137 if (interrupted) {
1138 Thread.currentThread().interrupt();
1139 }
1140 }
1141
1142 @Override
1143 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
1144 final RecipientId recipientId;
1145 try {
1146 recipientId = resolveRecipient(recipient);
1147 } catch (IOException e) {
1148 return false;
1149 }
1150 return contactHelper.isContactBlocked(recipientId);
1151 }
1152
1153 @Override
1154 public void sendContacts() throws IOException {
1155 syncHelper.sendContacts();
1156 }
1157
1158 @Override
1159 public List<Pair<RecipientAddress, Contact>> getContacts() {
1160 return account.getContactStore()
1161 .getContacts()
1162 .stream()
1163 .map(p -> new Pair<>(account.getRecipientStore().resolveRecipientAddress(p.first()), p.second()))
1164 .collect(Collectors.toList());
1165 }
1166
1167 @Override
1168 public String getContactOrProfileName(RecipientIdentifier.Single recipient) {
1169 final RecipientId recipientId;
1170 try {
1171 recipientId = resolveRecipient(recipient);
1172 } catch (IOException e) {
1173 return null;
1174 }
1175
1176 final var contact = account.getContactStore().getContact(recipientId);
1177 if (contact != null && !Util.isEmpty(contact.getName())) {
1178 return contact.getName();
1179 }
1180
1181 final var profile = getRecipientProfile(recipientId);
1182 if (profile != null) {
1183 return profile.getDisplayName();
1184 }
1185
1186 return null;
1187 }
1188
1189 @Override
1190 public Group getGroup(GroupId groupId) {
1191 return toGroup(groupHelper.getGroup(groupId));
1192 }
1193
1194 private GroupInfo getGroupInfo(GroupId groupId) {
1195 return groupHelper.getGroup(groupId);
1196 }
1197
1198 @Override
1199 public List<Identity> getIdentities() {
1200 return account.getIdentityKeyStore()
1201 .getIdentities()
1202 .stream()
1203 .map(this::toIdentity)
1204 .collect(Collectors.toList());
1205 }
1206
1207 private Identity toIdentity(final IdentityInfo identityInfo) {
1208 if (identityInfo == null) {
1209 return null;
1210 }
1211
1212 final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId());
1213 final var scannableFingerprint = identityHelper.computeSafetyNumberForScanning(identityInfo.getRecipientId(),
1214 identityInfo.getIdentityKey());
1215 return new Identity(address,
1216 identityInfo.getIdentityKey(),
1217 identityHelper.computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()),
1218 scannableFingerprint == null ? null : scannableFingerprint.getSerialized(),
1219 identityInfo.getTrustLevel(),
1220 identityInfo.getDateAdded());
1221 }
1222
1223 @Override
1224 public List<Identity> getIdentities(RecipientIdentifier.Single recipient) {
1225 IdentityInfo identity;
1226 try {
1227 identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient));
1228 } catch (IOException e) {
1229 identity = null;
1230 }
1231 return identity == null ? List.of() : List.of(toIdentity(identity));
1232 }
1233
1234 /**
1235 * Trust this the identity with this fingerprint
1236 *
1237 * @param recipient username of the identity
1238 * @param fingerprint Fingerprint
1239 */
1240 @Override
1241 public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) {
1242 RecipientId recipientId;
1243 try {
1244 recipientId = resolveRecipient(recipient);
1245 } catch (IOException e) {
1246 return false;
1247 }
1248 return identityHelper.trustIdentityVerified(recipientId, fingerprint);
1249 }
1250
1251 /**
1252 * Trust this the identity with this safety number
1253 *
1254 * @param recipient username of the identity
1255 * @param safetyNumber Safety number
1256 */
1257 @Override
1258 public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) {
1259 RecipientId recipientId;
1260 try {
1261 recipientId = resolveRecipient(recipient);
1262 } catch (IOException e) {
1263 return false;
1264 }
1265 return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber);
1266 }
1267
1268 /**
1269 * Trust this the identity with this scannable safety number
1270 *
1271 * @param recipient username of the identity
1272 * @param safetyNumber Scannable safety number
1273 */
1274 @Override
1275 public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) {
1276 RecipientId recipientId;
1277 try {
1278 recipientId = resolveRecipient(recipient);
1279 } catch (IOException e) {
1280 return false;
1281 }
1282 return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber);
1283 }
1284
1285 /**
1286 * Trust all keys of this identity without verification
1287 *
1288 * @param recipient username of the identity
1289 */
1290 @Override
1291 public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) {
1292 RecipientId recipientId;
1293 try {
1294 recipientId = resolveRecipient(recipient);
1295 } catch (IOException e) {
1296 return false;
1297 }
1298 return identityHelper.trustIdentityAllKeys(recipientId);
1299 }
1300
1301 private void handleIdentityFailure(
1302 final RecipientId recipientId,
1303 final org.whispersystems.signalservice.api.messages.SendMessageResult.IdentityFailure identityFailure
1304 ) {
1305 this.identityHelper.handleIdentityFailure(recipientId, identityFailure);
1306 }
1307
1308 private SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) {
1309 final var address = account.getRecipientStore().resolveRecipientAddress(recipientId);
1310 if (address.getUuid().isPresent()) {
1311 return address.toSignalServiceAddress();
1312 }
1313
1314 // Address in recipient store doesn't have a uuid, this shouldn't happen
1315 // Try to retrieve the uuid from the server
1316 final var number = address.getNumber().get();
1317 final ACI aci;
1318 try {
1319 aci = getRegisteredUser(number);
1320 } catch (IOException e) {
1321 logger.warn("Failed to get uuid for e164 number: {}", number, e);
1322 // Return SignalServiceAddress with unknown UUID
1323 return address.toSignalServiceAddress();
1324 }
1325 return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(aci));
1326 }
1327
1328 private Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws IOException {
1329 final var recipientIds = new HashSet<RecipientId>(recipients.size());
1330 for (var number : recipients) {
1331 final var recipientId = resolveRecipient(number);
1332 recipientIds.add(recipientId);
1333 }
1334 return recipientIds;
1335 }
1336
1337 private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws IOException {
1338 if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
1339 return account.getRecipientStore().resolveRecipient(ACI.from(uuidRecipient.uuid()));
1340 } else {
1341 final var number = ((RecipientIdentifier.Number) recipient).number();
1342 return account.getRecipientStore().resolveRecipient(number, () -> {
1343 try {
1344 return getRegisteredUser(number);
1345 } catch (IOException e) {
1346 return null;
1347 }
1348 });
1349 }
1350 }
1351
1352 private RecipientId resolveRecipient(RecipientAddress address) {
1353 return account.getRecipientStore().resolveRecipient(address);
1354 }
1355
1356 private RecipientId resolveRecipient(SignalServiceAddress address) {
1357 return account.getRecipientStore().resolveRecipient(address);
1358 }
1359
1360 private RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
1361 return account.getRecipientStore().resolveRecipientTrusted(address);
1362 }
1363
1364 @Override
1365 public void close() throws IOException {
1366 close(true);
1367 }
1368
1369 private void close(boolean closeAccount) throws IOException {
1370 Thread thread;
1371 synchronized (messageHandlers) {
1372 messageHandlers.clear();
1373 thread = receiveThread;
1374 receiveThread = null;
1375 }
1376 if (thread != null) {
1377 stopReceiveThread(thread);
1378 }
1379 executor.shutdown();
1380
1381 dependencies.getSignalWebSocket().disconnect();
1382
1383 if (closeAccount && account != null) {
1384 account.close();
1385 }
1386 account = null;
1387 }
1388 }