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