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