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