]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java
Implement change phone number
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / internal / ManagerImpl.java
1 /*
2 Copyright (C) 2015-2022 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.internal;
18
19 import org.asamk.signal.manager.Manager;
20 import org.asamk.signal.manager.api.AlreadyReceivingException;
21 import org.asamk.signal.manager.api.AttachmentInvalidException;
22 import org.asamk.signal.manager.api.CaptchaRequiredException;
23 import org.asamk.signal.manager.api.Configuration;
24 import org.asamk.signal.manager.api.Device;
25 import org.asamk.signal.manager.api.DeviceLinkUrl;
26 import org.asamk.signal.manager.api.Group;
27 import org.asamk.signal.manager.api.GroupId;
28 import org.asamk.signal.manager.api.GroupInviteLinkUrl;
29 import org.asamk.signal.manager.api.GroupNotFoundException;
30 import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
31 import org.asamk.signal.manager.api.Identity;
32 import org.asamk.signal.manager.api.IdentityVerificationCode;
33 import org.asamk.signal.manager.api.InactiveGroupLinkException;
34 import org.asamk.signal.manager.api.IncorrectPinException;
35 import org.asamk.signal.manager.api.InvalidDeviceLinkException;
36 import org.asamk.signal.manager.api.InvalidStickerException;
37 import org.asamk.signal.manager.api.InvalidUsernameException;
38 import org.asamk.signal.manager.api.LastGroupAdminException;
39 import org.asamk.signal.manager.api.Message;
40 import org.asamk.signal.manager.api.MessageEnvelope;
41 import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
42 import org.asamk.signal.manager.api.NotAGroupMemberException;
43 import org.asamk.signal.manager.api.NotPrimaryDeviceException;
44 import org.asamk.signal.manager.api.Pair;
45 import org.asamk.signal.manager.api.PendingAdminApprovalException;
46 import org.asamk.signal.manager.api.PinLockedException;
47 import org.asamk.signal.manager.api.Profile;
48 import org.asamk.signal.manager.api.RateLimitException;
49 import org.asamk.signal.manager.api.ReceiveConfig;
50 import org.asamk.signal.manager.api.Recipient;
51 import org.asamk.signal.manager.api.RecipientIdentifier;
52 import org.asamk.signal.manager.api.SendGroupMessageResults;
53 import org.asamk.signal.manager.api.SendMessageResult;
54 import org.asamk.signal.manager.api.SendMessageResults;
55 import org.asamk.signal.manager.api.StickerPackId;
56 import org.asamk.signal.manager.api.StickerPackInvalidException;
57 import org.asamk.signal.manager.api.StickerPackUrl;
58 import org.asamk.signal.manager.api.TextStyle;
59 import org.asamk.signal.manager.api.TypingAction;
60 import org.asamk.signal.manager.api.UnregisteredRecipientException;
61 import org.asamk.signal.manager.api.UpdateGroup;
62 import org.asamk.signal.manager.api.UpdateProfile;
63 import org.asamk.signal.manager.api.UserStatus;
64 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
65 import org.asamk.signal.manager.helper.AccountFileUpdater;
66 import org.asamk.signal.manager.helper.Context;
67 import org.asamk.signal.manager.storage.AttachmentStore;
68 import org.asamk.signal.manager.storage.AvatarStore;
69 import org.asamk.signal.manager.storage.SignalAccount;
70 import org.asamk.signal.manager.storage.groups.GroupInfo;
71 import org.asamk.signal.manager.storage.identities.IdentityInfo;
72 import org.asamk.signal.manager.storage.recipients.RecipientId;
73 import org.asamk.signal.manager.storage.stickerPacks.JsonStickerPack;
74 import org.asamk.signal.manager.storage.stickerPacks.StickerPackStore;
75 import org.asamk.signal.manager.storage.stickers.StickerPack;
76 import org.asamk.signal.manager.util.AttachmentUtils;
77 import org.asamk.signal.manager.util.KeyUtils;
78 import org.asamk.signal.manager.util.MimeUtils;
79 import org.asamk.signal.manager.util.StickerUtils;
80 import org.signal.libsignal.protocol.InvalidMessageException;
81 import org.signal.libsignal.usernames.BaseUsernameException;
82 import org.slf4j.Logger;
83 import org.slf4j.LoggerFactory;
84 import org.whispersystems.signalservice.api.SignalSessionLock;
85 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
86 import org.whispersystems.signalservice.api.messages.SignalServicePreview;
87 import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
88 import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
89 import org.whispersystems.signalservice.api.push.ServiceId;
90 import org.whispersystems.signalservice.api.push.ServiceId.ACI;
91 import org.whispersystems.signalservice.api.push.ServiceIdType;
92 import org.whispersystems.signalservice.api.util.DeviceNameUtil;
93 import org.whispersystems.signalservice.api.util.InvalidNumberException;
94 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
95 import org.whispersystems.signalservice.api.util.StreamDetails;
96 import org.whispersystems.signalservice.internal.util.Hex;
97 import org.whispersystems.signalservice.internal.util.Util;
98
99 import java.io.ByteArrayInputStream;
100 import java.io.File;
101 import java.io.IOException;
102 import java.io.InputStream;
103 import java.nio.charset.StandardCharsets;
104 import java.time.Duration;
105 import java.util.ArrayList;
106 import java.util.Collection;
107 import java.util.HashMap;
108 import java.util.HashSet;
109 import java.util.List;
110 import java.util.Map;
111 import java.util.Objects;
112 import java.util.Optional;
113 import java.util.Set;
114 import java.util.concurrent.ExecutorService;
115 import java.util.concurrent.Executors;
116 import java.util.concurrent.atomic.AtomicInteger;
117 import java.util.concurrent.locks.ReentrantLock;
118 import java.util.function.Function;
119 import java.util.stream.Collectors;
120 import java.util.stream.Stream;
121
122 import io.reactivex.rxjava3.disposables.CompositeDisposable;
123
124 public class ManagerImpl implements Manager {
125
126 private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class);
127
128 private SignalAccount account;
129 private final SignalDependencies dependencies;
130 private final Context context;
131
132 private final ExecutorService executor = Executors.newCachedThreadPool();
133
134 private Thread receiveThread;
135 private boolean isReceivingSynchronous;
136 private final Set<ReceiveMessageHandler> weakHandlers = new HashSet<>();
137 private final Set<ReceiveMessageHandler> messageHandlers = new HashSet<>();
138 private final List<Runnable> closedListeners = new ArrayList<>();
139 private final List<Runnable> addressChangedListeners = new ArrayList<>();
140 private final CompositeDisposable disposable = new CompositeDisposable();
141
142 public ManagerImpl(
143 SignalAccount account,
144 PathConfig pathConfig,
145 AccountFileUpdater accountFileUpdater,
146 ServiceEnvironmentConfig serviceEnvironmentConfig,
147 String userAgent
148 ) {
149 this.account = account;
150
151 final var sessionLock = new SignalSessionLock() {
152 private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
153
154 @Override
155 public Lock acquire() {
156 LEGACY_LOCK.lock();
157 return LEGACY_LOCK::unlock;
158 }
159 };
160 this.dependencies = new SignalDependencies(serviceEnvironmentConfig,
161 userAgent,
162 account.getCredentialsProvider(),
163 account.getSignalServiceDataStore(),
164 executor,
165 sessionLock);
166 final var avatarStore = new AvatarStore(pathConfig.avatarsPath());
167 final var attachmentStore = new AttachmentStore(pathConfig.attachmentsPath());
168 final var stickerPackStore = new StickerPackStore(pathConfig.stickerPacksPath());
169
170 this.context = new Context(account, new AccountFileUpdater() {
171 @Override
172 public void updateAccountIdentifiers(final String number, final ACI aci) {
173 accountFileUpdater.updateAccountIdentifiers(number, aci);
174 synchronized (addressChangedListeners) {
175 addressChangedListeners.forEach(Runnable::run);
176 }
177 }
178
179 @Override
180 public void removeAccount() {
181 accountFileUpdater.removeAccount();
182 }
183 }, dependencies, avatarStore, attachmentStore, stickerPackStore);
184 this.context.getAccountHelper().setUnregisteredListener(this::close);
185 this.context.getReceiveHelper().setAuthenticationFailureListener(this::close);
186 this.context.getReceiveHelper().setCaughtUpWithOldMessagesListener(() -> {
187 synchronized (this) {
188 this.notifyAll();
189 }
190 });
191 disposable.add(account.getIdentityKeyStore().getIdentityChanges().subscribe(serviceId -> {
192 logger.trace("Archiving old sessions for {}", serviceId);
193 account.getAccountData(ServiceIdType.ACI).getSessionStore().archiveSessions(serviceId);
194 account.getAccountData(ServiceIdType.PNI).getSessionStore().archiveSessions(serviceId);
195 account.getSenderKeyStore().deleteSharedWith(serviceId);
196 final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId);
197 final var profile = account.getProfileStore().getProfile(recipientId);
198 if (profile != null) {
199 account.getProfileStore()
200 .storeProfile(recipientId,
201 Profile.newBuilder(profile)
202 .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN)
203 .withLastUpdateTimestamp(0)
204 .build());
205 }
206 }));
207 }
208
209 @Override
210 public String getSelfNumber() {
211 return account.getNumber();
212 }
213
214 public void checkAccountState() throws IOException {
215 context.getAccountHelper().checkAccountState();
216 }
217
218 @Override
219 public Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException {
220 final var canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> {
221 try {
222 final var canonicalizedNumber = PhoneNumberFormatter.formatNumber(n, account.getNumber());
223 if (!canonicalizedNumber.equals(n)) {
224 logger.debug("Normalized number {} to {}.", n, canonicalizedNumber);
225 }
226 return canonicalizedNumber;
227 } catch (InvalidNumberException e) {
228 return "";
229 }
230 }));
231
232 // Note "registeredUsers" has no optionals. It only gives us info on users who are registered
233 final var canonicalizedNumbersSet = canonicalizedNumbers.values()
234 .stream()
235 .filter(s -> !s.isEmpty())
236 .collect(Collectors.toSet());
237 final var registeredUsers = context.getRecipientHelper().getRegisteredUsers(canonicalizedNumbersSet);
238
239 return numbers.stream().collect(Collectors.toMap(n -> n, n -> {
240 final var number = canonicalizedNumbers.get(n);
241 final var user = registeredUsers.get(number);
242 final var serviceId = user == null ? null : user.getServiceId();
243 final var profile = serviceId == null
244 ? null
245 : context.getProfileHelper()
246 .getRecipientProfile(account.getRecipientResolver().resolveRecipient(serviceId));
247 return new UserStatus(number.isEmpty() ? null : number,
248 serviceId == null ? null : serviceId.getRawUuid(),
249 profile != null
250 && profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED);
251 }));
252 }
253
254 @Override
255 public void updateAccountAttributes(String deviceName) throws IOException {
256 if (deviceName != null) {
257 context.getAccountHelper().setDeviceName(deviceName);
258 }
259 context.getAccountHelper().updateAccountAttributes();
260 context.getAccountHelper().checkWhoAmiI();
261 }
262
263 @Override
264 public Configuration getConfiguration() {
265 final var configurationStore = account.getConfigurationStore();
266 return Configuration.from(configurationStore);
267 }
268
269 @Override
270 public void updateConfiguration(
271 Configuration configuration
272 ) throws NotPrimaryDeviceException {
273 if (!account.isPrimaryDevice()) {
274 throw new NotPrimaryDeviceException();
275 }
276
277 final var configurationStore = account.getConfigurationStore();
278 if (configuration.readReceipts().isPresent()) {
279 configurationStore.setReadReceipts(configuration.readReceipts().get());
280 }
281 if (configuration.unidentifiedDeliveryIndicators().isPresent()) {
282 configurationStore.setUnidentifiedDeliveryIndicators(configuration.unidentifiedDeliveryIndicators().get());
283 }
284 if (configuration.typingIndicators().isPresent()) {
285 configurationStore.setTypingIndicators(configuration.typingIndicators().get());
286 }
287 if (configuration.linkPreviews().isPresent()) {
288 configurationStore.setLinkPreviews(configuration.linkPreviews().get());
289 }
290 context.getSyncHelper().sendConfigurationMessage();
291 }
292
293 @Override
294 public void updateProfile(UpdateProfile updateProfile) throws IOException {
295 context.getProfileHelper()
296 .setProfile(updateProfile.getGivenName(),
297 updateProfile.getFamilyName(),
298 updateProfile.getAbout(),
299 updateProfile.getAboutEmoji(),
300 updateProfile.isDeleteAvatar()
301 ? Optional.empty()
302 : updateProfile.getAvatar() == null ? null : Optional.of(updateProfile.getAvatar()),
303 updateProfile.getMobileCoinAddress());
304 context.getSyncHelper().sendSyncFetchProfileMessage();
305 }
306
307 void refreshCurrentUsername() throws IOException, BaseUsernameException {
308 context.getAccountHelper().refreshCurrentUsername();
309 }
310
311 @Override
312 public String setUsername(final String username) throws IOException, InvalidUsernameException {
313 try {
314 return context.getAccountHelper().reserveUsername(username);
315 } catch (BaseUsernameException e) {
316 throw new InvalidUsernameException(e.getMessage() + " (" + e.getClass().getSimpleName() + ")", e);
317 }
318 }
319
320 @Override
321 public void deleteUsername() throws IOException {
322 context.getAccountHelper().deleteUsername();
323 }
324
325 @Override
326 public void startChangeNumber(
327 String newNumber, boolean voiceVerification, String captcha
328 ) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException {
329 if (!account.isPrimaryDevice()) {
330 throw new NotPrimaryDeviceException();
331 }
332 context.getAccountHelper().startChangeNumber(newNumber, voiceVerification, captcha);
333 }
334
335 @Override
336 public void finishChangeNumber(
337 String newNumber, String verificationCode, String pin
338 ) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException {
339 if (!account.isPrimaryDevice()) {
340 throw new NotPrimaryDeviceException();
341 }
342 context.getAccountHelper().finishChangeNumber(newNumber, verificationCode, pin);
343 }
344
345 @Override
346 public void unregister() throws IOException {
347 context.getAccountHelper().unregister();
348 }
349
350 @Override
351 public void deleteAccount() throws IOException {
352 context.getAccountHelper().deleteAccount();
353 }
354
355 @Override
356 public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException {
357 captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
358
359 dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
360 }
361
362 @Override
363 public List<Device> getLinkedDevices() throws IOException {
364 var devices = dependencies.getAccountManager().getDevices();
365 account.setMultiDevice(devices.size() > 1);
366 var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
367 return devices.stream().map(d -> {
368 String deviceName = d.getName();
369 if (deviceName != null) {
370 try {
371 deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey);
372 } catch (IOException e) {
373 logger.debug("Failed to decrypt device name, maybe plain text?", e);
374 }
375 }
376 return new Device(d.getId(),
377 deviceName,
378 d.getCreated(),
379 d.getLastSeen(),
380 d.getId() == account.getDeviceId());
381 }).toList();
382 }
383
384 @Override
385 public void removeLinkedDevices(int deviceId) throws IOException {
386 context.getAccountHelper().removeLinkedDevices(deviceId);
387 }
388
389 @Override
390 public void addDeviceLink(DeviceLinkUrl linkUrl) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException {
391 if (!account.isPrimaryDevice()) {
392 throw new NotPrimaryDeviceException();
393 }
394 context.getAccountHelper().addDevice(linkUrl);
395 }
396
397 @Override
398 public void setRegistrationLockPin(Optional<String> pin) throws IOException, NotPrimaryDeviceException {
399 if (!account.isPrimaryDevice()) {
400 throw new NotPrimaryDeviceException();
401 }
402 if (pin.isPresent()) {
403 context.getAccountHelper().setRegistrationPin(pin.get());
404 } else {
405 context.getAccountHelper().removeRegistrationPin();
406 }
407 }
408
409 void refreshPreKeys() throws IOException {
410 context.getPreKeyHelper().refreshPreKeysIfNecessary();
411 }
412
413 @Override
414 public List<Group> getGroups() {
415 return account.getGroupStore().getGroups().stream().map(this::toGroup).toList();
416 }
417
418 private Group toGroup(final GroupInfo groupInfo) {
419 if (groupInfo == null) {
420 return null;
421 }
422
423 return Group.from(groupInfo, account.getRecipientAddressResolver(), account.getSelfRecipientId());
424 }
425
426 @Override
427 public SendGroupMessageResults quitGroup(
428 GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins
429 ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException {
430 final var newAdmins = context.getRecipientHelper().resolveRecipients(groupAdmins);
431 return context.getGroupHelper().quitGroup(groupId, newAdmins);
432 }
433
434 @Override
435 public void deleteGroup(GroupId groupId) throws IOException {
436 final var group = context.getGroupHelper().getGroup(groupId);
437 if (group.isMember(account.getSelfRecipientId())) {
438 throw new IOException(
439 "The local group information cannot be removed, as the user is still a member of the group");
440 }
441 context.getGroupHelper().deleteGroup(groupId);
442 }
443
444 @Override
445 public Pair<GroupId, SendGroupMessageResults> createGroup(
446 String name, Set<RecipientIdentifier.Single> members, String avatarFile
447 ) throws IOException, AttachmentInvalidException, UnregisteredRecipientException {
448 return context.getGroupHelper()
449 .createGroup(name,
450 members == null ? null : context.getRecipientHelper().resolveRecipients(members),
451 avatarFile);
452 }
453
454 @Override
455 public SendGroupMessageResults updateGroup(
456 final GroupId groupId, final UpdateGroup updateGroup
457 ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException {
458 return context.getGroupHelper()
459 .updateGroup(groupId,
460 updateGroup.getName(),
461 updateGroup.getDescription(),
462 updateGroup.getMembers() == null
463 ? null
464 : context.getRecipientHelper().resolveRecipients(updateGroup.getMembers()),
465 updateGroup.getRemoveMembers() == null
466 ? null
467 : context.getRecipientHelper().resolveRecipients(updateGroup.getRemoveMembers()),
468 updateGroup.getAdmins() == null
469 ? null
470 : context.getRecipientHelper().resolveRecipients(updateGroup.getAdmins()),
471 updateGroup.getRemoveAdmins() == null
472 ? null
473 : context.getRecipientHelper().resolveRecipients(updateGroup.getRemoveAdmins()),
474 updateGroup.getBanMembers() == null
475 ? null
476 : context.getRecipientHelper().resolveRecipients(updateGroup.getBanMembers()),
477 updateGroup.getUnbanMembers() == null
478 ? null
479 : context.getRecipientHelper().resolveRecipients(updateGroup.getUnbanMembers()),
480 updateGroup.isResetGroupLink(),
481 updateGroup.getGroupLinkState(),
482 updateGroup.getAddMemberPermission(),
483 updateGroup.getEditDetailsPermission(),
484 updateGroup.getAvatarFile(),
485 updateGroup.getExpirationTimer(),
486 updateGroup.getIsAnnouncementGroup());
487 }
488
489 @Override
490 public Pair<GroupId, SendGroupMessageResults> joinGroup(
491 GroupInviteLinkUrl inviteLinkUrl
492 ) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException {
493 return context.getGroupHelper().joinGroup(inviteLinkUrl);
494 }
495
496 private SendMessageResults sendMessage(
497 SignalServiceDataMessage.Builder messageBuilder, Set<RecipientIdentifier> recipients
498 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
499 return sendMessage(messageBuilder, recipients, Optional.empty());
500 }
501
502 private SendMessageResults sendMessage(
503 SignalServiceDataMessage.Builder messageBuilder,
504 Set<RecipientIdentifier> recipients,
505 Optional<Long> editTargetTimestamp
506 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
507 var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
508 long timestamp = System.currentTimeMillis();
509 messageBuilder.withTimestamp(timestamp);
510 for (final var recipient : recipients) {
511 if (recipient instanceof RecipientIdentifier.Single single) {
512 try {
513 final var recipientId = context.getRecipientHelper().resolveRecipient(single);
514 final var result = context.getSendHelper()
515 .sendMessage(messageBuilder, recipientId, editTargetTimestamp);
516 results.put(recipient, List.of(toSendMessageResult(result)));
517 } catch (UnregisteredRecipientException e) {
518 results.put(recipient,
519 List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress())));
520 }
521 } else if (recipient instanceof RecipientIdentifier.NoteToSelf) {
522 final var result = context.getSendHelper().sendSelfMessage(messageBuilder, editTargetTimestamp);
523 results.put(recipient, List.of(toSendMessageResult(result)));
524 } else if (recipient instanceof RecipientIdentifier.Group group) {
525 final var result = context.getSendHelper()
526 .sendAsGroupMessage(messageBuilder, group.groupId(), editTargetTimestamp);
527 results.put(recipient, result.stream().map(this::toSendMessageResult).toList());
528 }
529 }
530 return new SendMessageResults(timestamp, results);
531 }
532
533 private SendMessageResult toSendMessageResult(final org.whispersystems.signalservice.api.messages.SendMessageResult result) {
534 return SendMessageResult.from(result, account.getRecipientResolver(), account.getRecipientAddressResolver());
535 }
536
537 private SendMessageResults sendTypingMessage(
538 SignalServiceTypingMessage.Action action, Set<RecipientIdentifier> recipients
539 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
540 var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
541 final var timestamp = System.currentTimeMillis();
542 for (var recipient : recipients) {
543 if (recipient instanceof RecipientIdentifier.Single single) {
544 final var message = new SignalServiceTypingMessage(action, timestamp, Optional.empty());
545 try {
546 final var recipientId = context.getRecipientHelper().resolveRecipient(single);
547 final var result = context.getSendHelper().sendTypingMessage(message, recipientId);
548 results.put(recipient, List.of(toSendMessageResult(result)));
549 } catch (UnregisteredRecipientException e) {
550 results.put(recipient,
551 List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress())));
552 }
553 } else if (recipient instanceof RecipientIdentifier.Group) {
554 final var groupId = ((RecipientIdentifier.Group) recipient).groupId();
555 final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize()));
556 final var result = context.getSendHelper().sendGroupTypingMessage(message, groupId);
557 results.put(recipient, result.stream().map(this::toSendMessageResult).toList());
558 }
559 }
560 return new SendMessageResults(timestamp, results);
561 }
562
563 @Override
564 public SendMessageResults sendTypingMessage(
565 TypingAction action, Set<RecipientIdentifier> recipients
566 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
567 return sendTypingMessage(action.toSignalService(), recipients);
568 }
569
570 @Override
571 public SendMessageResults sendReadReceipt(
572 RecipientIdentifier.Single sender, List<Long> messageIds
573 ) {
574 final var timestamp = System.currentTimeMillis();
575 var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
576 messageIds,
577 timestamp);
578
579 return sendReceiptMessage(sender, timestamp, receiptMessage);
580 }
581
582 @Override
583 public SendMessageResults sendViewedReceipt(
584 RecipientIdentifier.Single sender, List<Long> messageIds
585 ) {
586 final var timestamp = System.currentTimeMillis();
587 var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
588 messageIds,
589 timestamp);
590
591 return sendReceiptMessage(sender, timestamp, receiptMessage);
592 }
593
594 private SendMessageResults sendReceiptMessage(
595 final RecipientIdentifier.Single sender,
596 final long timestamp,
597 final SignalServiceReceiptMessage receiptMessage
598 ) {
599 try {
600 final var result = context.getSendHelper()
601 .sendReceiptMessage(receiptMessage, context.getRecipientHelper().resolveRecipient(sender));
602 return new SendMessageResults(timestamp, Map.of(sender, List.of(toSendMessageResult(result))));
603 } catch (UnregisteredRecipientException e) {
604 return new SendMessageResults(timestamp,
605 Map.of(sender, List.of(SendMessageResult.unregisteredFailure(sender.toPartialRecipientAddress()))));
606 }
607 }
608
609 @Override
610 public SendMessageResults sendMessage(
611 Message message, Set<RecipientIdentifier> recipients
612 ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
613 final var selfProfile = context.getProfileHelper().getSelfProfile();
614 if (selfProfile == null || selfProfile.getDisplayName().isEmpty()) {
615 logger.warn(
616 "No profile name set. When sending a message it's recommended to set a profile name with the updateProfile command. This may become mandatory in the future.");
617 }
618 final var messageBuilder = SignalServiceDataMessage.newBuilder();
619 applyMessage(messageBuilder, message);
620 return sendMessage(messageBuilder, recipients);
621 }
622
623 @Override
624 public SendMessageResults sendEditMessage(
625 Message message, Set<RecipientIdentifier> recipients, long editTargetTimestamp
626 ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
627 final var messageBuilder = SignalServiceDataMessage.newBuilder();
628 applyMessage(messageBuilder, message);
629 return sendMessage(messageBuilder, recipients, Optional.of(editTargetTimestamp));
630 }
631
632 private void applyMessage(
633 final SignalServiceDataMessage.Builder messageBuilder, final Message message
634 ) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException {
635 if (message.messageText().length() > 2000) {
636 final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8);
637 final var textAttachment = AttachmentUtils.createAttachmentStream(new StreamDetails(new ByteArrayInputStream(
638 messageBytes), MimeUtils.LONG_TEXT, messageBytes.length), Optional.empty());
639 messageBuilder.withBody(message.messageText().substring(0, 2000));
640 messageBuilder.withAttachment(context.getAttachmentHelper().uploadAttachment(textAttachment));
641 } else {
642 messageBuilder.withBody(message.messageText());
643 }
644 if (message.attachments().size() > 0) {
645 messageBuilder.withAttachments(context.getAttachmentHelper().uploadAttachments(message.attachments()));
646 }
647 if (message.mentions().size() > 0) {
648 messageBuilder.withMentions(resolveMentions(message.mentions()));
649 }
650 if (message.textStyles().size() > 0) {
651 messageBuilder.withBodyRanges(message.textStyles().stream().map(TextStyle::toBodyRange).toList());
652 }
653 if (message.quote().isPresent()) {
654 final var quote = message.quote().get();
655 final var quotedAttachments = new ArrayList<SignalServiceDataMessage.Quote.QuotedAttachment>();
656 for (final var a : quote.attachments()) {
657 final var quotedAttachment = new SignalServiceDataMessage.Quote.QuotedAttachment(a.contentType(),
658 a.filename(),
659 a.preview() == null ? null : context.getAttachmentHelper().uploadAttachment(a.preview()));
660 quotedAttachments.add(quotedAttachment);
661 }
662 messageBuilder.withQuote(new SignalServiceDataMessage.Quote(quote.timestamp(),
663 context.getRecipientHelper()
664 .resolveSignalServiceAddress(context.getRecipientHelper().resolveRecipient(quote.author()))
665 .getServiceId(),
666 quote.message(),
667 quotedAttachments,
668 resolveMentions(quote.mentions()),
669 SignalServiceDataMessage.Quote.Type.NORMAL,
670 quote.textStyles().stream().map(TextStyle::toBodyRange).toList()));
671 }
672 if (message.sticker().isPresent()) {
673 final var sticker = message.sticker().get();
674 final var packId = StickerPackId.deserialize(sticker.packId());
675 final var stickerId = sticker.stickerId();
676
677 final var stickerPack = context.getAccount().getStickerStore().getStickerPack(packId);
678 if (stickerPack == null) {
679 throw new InvalidStickerException("Sticker pack not found");
680 }
681 final var manifest = context.getStickerHelper().getOrRetrieveStickerPack(packId, stickerPack.packKey());
682 if (manifest.stickers().size() <= stickerId) {
683 throw new InvalidStickerException("Sticker id not part of this pack");
684 }
685 final var manifestSticker = manifest.stickers().get(stickerId);
686 final var streamDetails = context.getStickerPackStore().retrieveSticker(packId, stickerId);
687 if (streamDetails == null) {
688 throw new InvalidStickerException("Missing local sticker file");
689 }
690 messageBuilder.withSticker(new SignalServiceDataMessage.Sticker(packId.serialize(),
691 stickerPack.packKey(),
692 stickerId,
693 manifestSticker.emoji(),
694 AttachmentUtils.createAttachmentStream(streamDetails, Optional.empty())));
695 }
696 if (message.previews().size() > 0) {
697 final var previews = new ArrayList<SignalServicePreview>(message.previews().size());
698 for (final var p : message.previews()) {
699 final var image = p.image().isPresent() ? context.getAttachmentHelper()
700 .uploadAttachment(p.image().get()) : null;
701 previews.add(new SignalServicePreview(p.url(),
702 p.title(),
703 p.description(),
704 0,
705 Optional.ofNullable(image)));
706 }
707 messageBuilder.withPreviews(previews);
708 }
709 if (message.storyReply().isPresent()) {
710 final var storyReply = message.storyReply().get();
711 final var authorServiceId = context.getRecipientHelper()
712 .resolveSignalServiceAddress(context.getRecipientHelper().resolveRecipient(storyReply.author()))
713 .getServiceId();
714 messageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(authorServiceId,
715 storyReply.timestamp()));
716 }
717 }
718
719 private ArrayList<SignalServiceDataMessage.Mention> resolveMentions(final List<Message.Mention> mentionList) throws UnregisteredRecipientException {
720 final var mentions = new ArrayList<SignalServiceDataMessage.Mention>();
721 for (final var m : mentionList) {
722 final var recipientId = context.getRecipientHelper().resolveRecipient(m.recipient());
723 mentions.add(new SignalServiceDataMessage.Mention(context.getRecipientHelper()
724 .resolveSignalServiceAddress(recipientId)
725 .getServiceId(), m.start(), m.length()));
726 }
727 return mentions;
728 }
729
730 @Override
731 public SendMessageResults sendRemoteDeleteMessage(
732 long targetSentTimestamp, Set<RecipientIdentifier> recipients
733 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
734 var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
735 final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
736 for (final var recipient : recipients) {
737 if (recipient instanceof RecipientIdentifier.Uuid u) {
738 account.getMessageSendLogStore()
739 .deleteEntryForRecipientNonGroup(targetSentTimestamp, ACI.from(u.uuid()));
740 } else if (recipient instanceof RecipientIdentifier.Single r) {
741 try {
742 final var recipientId = context.getRecipientHelper().resolveRecipient(r);
743 final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
744 if (address.serviceId().isPresent()) {
745 account.getMessageSendLogStore()
746 .deleteEntryForRecipientNonGroup(targetSentTimestamp, address.serviceId().get());
747 }
748 } catch (UnregisteredRecipientException ignored) {
749 }
750 } else if (recipient instanceof RecipientIdentifier.Group r) {
751 account.getMessageSendLogStore().deleteEntryForGroup(targetSentTimestamp, r.groupId());
752 }
753 }
754 return sendMessage(messageBuilder, recipients);
755 }
756
757 @Override
758 public SendMessageResults sendMessageReaction(
759 String emoji,
760 boolean remove,
761 RecipientIdentifier.Single targetAuthor,
762 long targetSentTimestamp,
763 Set<RecipientIdentifier> recipients,
764 final boolean isStory
765 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
766 var targetAuthorRecipientId = context.getRecipientHelper().resolveRecipient(targetAuthor);
767 final var authorServiceId = context.getRecipientHelper()
768 .resolveSignalServiceAddress(targetAuthorRecipientId)
769 .getServiceId();
770 var reaction = new SignalServiceDataMessage.Reaction(emoji, remove, authorServiceId, targetSentTimestamp);
771 final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction);
772 if (isStory) {
773 messageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(authorServiceId,
774 targetSentTimestamp));
775 }
776 return sendMessage(messageBuilder, recipients);
777 }
778
779 @Override
780 public SendMessageResults sendPaymentNotificationMessage(
781 byte[] receipt, String note, RecipientIdentifier.Single recipient
782 ) throws IOException {
783 final var paymentNotification = new SignalServiceDataMessage.PaymentNotification(receipt, note);
784 final var payment = new SignalServiceDataMessage.Payment(paymentNotification, null);
785 final var messageBuilder = SignalServiceDataMessage.newBuilder().withPayment(payment);
786 try {
787 return sendMessage(messageBuilder, Set.of(recipient));
788 } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) {
789 throw new AssertionError(e);
790 }
791 }
792
793 @Override
794 public SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException {
795 var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
796
797 try {
798 return sendMessage(messageBuilder,
799 recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()));
800 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
801 throw new AssertionError(e);
802 } finally {
803 for (var recipient : recipients) {
804 final RecipientId recipientId;
805 try {
806 recipientId = context.getRecipientHelper().resolveRecipient(recipient);
807 } catch (UnregisteredRecipientException e) {
808 continue;
809 }
810 final var serviceId = context.getAccount()
811 .getRecipientAddressResolver()
812 .resolveRecipientAddress(recipientId)
813 .serviceId();
814 if (serviceId.isPresent()) {
815 account.getAccountData(ServiceIdType.ACI).getSessionStore().deleteAllSessions(serviceId.get());
816 }
817 }
818 }
819 }
820
821 @Override
822 public void deleteRecipient(final RecipientIdentifier.Single recipient) {
823 final var recipientIdOptional = context.getRecipientHelper().resolveRecipientOptional(recipient);
824 if (recipientIdOptional.isPresent()) {
825 account.removeRecipient(recipientIdOptional.get());
826 }
827 }
828
829 @Override
830 public void deleteContact(final RecipientIdentifier.Single recipient) {
831 final var recipientIdOptional = context.getRecipientHelper().resolveRecipientOptional(recipient);
832 if (recipientIdOptional.isPresent()) {
833 account.getContactStore().deleteContact(recipientIdOptional.get());
834 }
835 }
836
837 @Override
838 public void setContactName(
839 RecipientIdentifier.Single recipient, String givenName, final String familyName
840 ) throws NotPrimaryDeviceException, UnregisteredRecipientException {
841 if (!account.isPrimaryDevice()) {
842 throw new NotPrimaryDeviceException();
843 }
844 context.getContactHelper()
845 .setContactName(context.getRecipientHelper().resolveRecipient(recipient), givenName, familyName);
846 }
847
848 @Override
849 public void setContactsBlocked(
850 Collection<RecipientIdentifier.Single> recipients, boolean blocked
851 ) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException {
852 if (!account.isPrimaryDevice()) {
853 throw new NotPrimaryDeviceException();
854 }
855 if (recipients.size() == 0) {
856 return;
857 }
858 final var recipientIds = context.getRecipientHelper().resolveRecipients(recipients);
859 final var selfRecipientId = account.getSelfRecipientId();
860 boolean shouldRotateProfileKey = false;
861 for (final var recipientId : recipientIds) {
862 if (context.getContactHelper().isContactBlocked(recipientId) == blocked) {
863 continue;
864 }
865 context.getContactHelper().setContactBlocked(recipientId, blocked);
866 // if we don't have a common group with the blocked contact we need to rotate the profile key
867 shouldRotateProfileKey = blocked && (
868 shouldRotateProfileKey || account.getGroupStore()
869 .getGroups()
870 .stream()
871 .noneMatch(g -> g.isMember(selfRecipientId) && g.isMember(recipientId))
872 );
873 }
874 if (shouldRotateProfileKey) {
875 context.getProfileHelper().rotateProfileKey();
876 }
877 context.getSyncHelper().sendBlockedList();
878 }
879
880 @Override
881 public void setGroupsBlocked(
882 final Collection<GroupId> groupIds, final boolean blocked
883 ) throws GroupNotFoundException, NotPrimaryDeviceException, IOException {
884 if (!account.isPrimaryDevice()) {
885 throw new NotPrimaryDeviceException();
886 }
887 if (groupIds.size() == 0) {
888 return;
889 }
890 boolean shouldRotateProfileKey = false;
891 for (final var groupId : groupIds) {
892 if (context.getGroupHelper().isGroupBlocked(groupId) == blocked) {
893 continue;
894 }
895 context.getGroupHelper().setGroupBlocked(groupId, blocked);
896 shouldRotateProfileKey = blocked;
897 }
898 if (shouldRotateProfileKey) {
899 context.getProfileHelper().rotateProfileKey();
900 }
901 context.getSyncHelper().sendBlockedList();
902 }
903
904 @Override
905 public void setExpirationTimer(
906 RecipientIdentifier.Single recipient, int messageExpirationTimer
907 ) throws IOException, UnregisteredRecipientException {
908 var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
909 context.getContactHelper().setExpirationTimer(recipientId, messageExpirationTimer);
910 final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
911 try {
912 sendMessage(messageBuilder, Set.of(recipient));
913 } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) {
914 throw new AssertionError(e);
915 }
916 }
917
918 @Override
919 public StickerPackUrl uploadStickerPack(File path) throws IOException, StickerPackInvalidException {
920 var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path);
921
922 var messageSender = dependencies.getMessageSender();
923
924 var packKey = KeyUtils.createStickerUploadKey();
925 var packIdString = messageSender.uploadStickerManifest(manifest, packKey);
926 var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString));
927
928 var sticker = new StickerPack(packId, packKey);
929 account.getStickerStore().addStickerPack(sticker);
930 context.getSyncHelper().sendStickerOperationsMessage(List.of(sticker), List.of());
931
932 return new StickerPackUrl(packId, packKey);
933 }
934
935 @Override
936 public void installStickerPack(StickerPackUrl url) throws IOException {
937 final var packId = url.packId();
938 final var packKey = url.packKey();
939 try {
940 context.getStickerHelper().retrieveStickerPack(packId, packKey);
941 } catch (InvalidMessageException e) {
942 throw new IOException(e);
943 }
944
945 final var sticker = context.getStickerHelper().addOrUpdateStickerPack(packId, packKey, true);
946 context.getSyncHelper().sendStickerOperationsMessage(List.of(sticker), List.of());
947 }
948
949 @Override
950 public List<org.asamk.signal.manager.api.StickerPack> getStickerPacks() {
951 final var stickerPackStore = context.getStickerPackStore();
952 return account.getStickerStore().getStickerPacks().stream().map(pack -> {
953 if (stickerPackStore.existsStickerPack(pack.packId())) {
954 try {
955 final var manifest = stickerPackStore.retrieveManifest(pack.packId());
956 return new org.asamk.signal.manager.api.StickerPack(pack.packId(),
957 new StickerPackUrl(pack.packId(), pack.packKey()),
958 pack.isInstalled(),
959 manifest.title(),
960 manifest.author(),
961 Optional.ofNullable(manifest.cover() == null ? null : manifest.cover().toApi()),
962 manifest.stickers().stream().map(JsonStickerPack.JsonSticker::toApi).toList());
963 } catch (Exception e) {
964 logger.warn("Failed to read local sticker pack manifest: {}", e.getMessage(), e);
965 }
966 }
967
968 return new org.asamk.signal.manager.api.StickerPack(pack.packId(), pack.packKey(), pack.isInstalled());
969 }).toList();
970 }
971
972 @Override
973 public void requestAllSyncData() throws IOException {
974 context.getSyncHelper().requestAllSyncData();
975 retrieveRemoteStorage();
976 }
977
978 void retrieveRemoteStorage() throws IOException {
979 context.getStorageHelper().readDataFromStorage();
980 }
981
982 @Override
983 public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
984 synchronized (messageHandlers) {
985 if (isWeakListener) {
986 weakHandlers.add(handler);
987 } else {
988 messageHandlers.add(handler);
989 startReceiveThreadIfRequired();
990 }
991 }
992 }
993
994 private static final AtomicInteger threadNumber = new AtomicInteger(0);
995
996 private void startReceiveThreadIfRequired() {
997 if (receiveThread != null || isReceivingSynchronous) {
998 return;
999 }
1000 receiveThread = new Thread(() -> {
1001 logger.debug("Starting receiving messages");
1002 context.getReceiveHelper().receiveMessagesContinuously(this::passReceivedMessageToHandlers);
1003 logger.debug("Finished receiving messages");
1004 synchronized (messageHandlers) {
1005 receiveThread = null;
1006
1007 // Check if in the meantime another handler has been registered
1008 if (!messageHandlers.isEmpty()) {
1009 logger.debug("Another handler has been registered, starting receive thread again");
1010 startReceiveThreadIfRequired();
1011 }
1012 }
1013 });
1014 receiveThread.setName("receive-" + threadNumber.getAndIncrement());
1015
1016 receiveThread.start();
1017 }
1018
1019 private void passReceivedMessageToHandlers(MessageEnvelope envelope, Throwable e) {
1020 synchronized (messageHandlers) {
1021 Stream.concat(messageHandlers.stream(), weakHandlers.stream()).forEach(h -> {
1022 try {
1023 h.handleMessage(envelope, e);
1024 } catch (Throwable ex) {
1025 logger.warn("Message handler failed, ignoring", ex);
1026 }
1027 });
1028 }
1029 }
1030
1031 @Override
1032 public void removeReceiveHandler(final ReceiveMessageHandler handler) {
1033 final Thread thread;
1034 synchronized (messageHandlers) {
1035 weakHandlers.remove(handler);
1036 messageHandlers.remove(handler);
1037 if (!messageHandlers.isEmpty() || receiveThread == null || isReceivingSynchronous) {
1038 return;
1039 }
1040 thread = receiveThread;
1041 receiveThread = null;
1042 }
1043
1044 stopReceiveThread(thread);
1045 }
1046
1047 private void stopReceiveThread(final Thread thread) {
1048 if (context.getReceiveHelper().requestStopReceiveMessages()) {
1049 logger.debug("Receive stop requested, interrupting read from server.");
1050 thread.interrupt();
1051 }
1052 try {
1053 thread.join();
1054 } catch (InterruptedException ignored) {
1055 }
1056 }
1057
1058 @Override
1059 public boolean isReceiving() {
1060 if (isReceivingSynchronous) {
1061 return true;
1062 }
1063 synchronized (messageHandlers) {
1064 return messageHandlers.size() > 0;
1065 }
1066 }
1067
1068 @Override
1069 public void receiveMessages(
1070 Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler
1071 ) throws IOException, AlreadyReceivingException {
1072 receiveMessages(timeout.orElse(Duration.ofMinutes(1)), timeout.isPresent(), maxMessages.orElse(null), handler);
1073 }
1074
1075 private void receiveMessages(
1076 Duration timeout, boolean returnOnTimeout, Integer maxMessages, ReceiveMessageHandler handler
1077 ) throws IOException, AlreadyReceivingException {
1078 synchronized (messageHandlers) {
1079 if (isReceiving()) {
1080 throw new AlreadyReceivingException("Already receiving message.");
1081 }
1082 isReceivingSynchronous = true;
1083 receiveThread = Thread.currentThread();
1084 }
1085 try {
1086 context.getReceiveHelper().receiveMessages(timeout, returnOnTimeout, maxMessages, (envelope, e) -> {
1087 passReceivedMessageToHandlers(envelope, e);
1088 handler.handleMessage(envelope, e);
1089 });
1090 } finally {
1091 synchronized (messageHandlers) {
1092 receiveThread = null;
1093 isReceivingSynchronous = false;
1094 if (messageHandlers.size() > 0) {
1095 startReceiveThreadIfRequired();
1096 }
1097 }
1098 }
1099 }
1100
1101 @Override
1102 public void setReceiveConfig(final ReceiveConfig receiveConfig) {
1103 context.getReceiveHelper().setReceiveConfig(receiveConfig);
1104 }
1105
1106 @Override
1107 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
1108 final RecipientId recipientId;
1109 try {
1110 recipientId = context.getRecipientHelper().resolveRecipient(recipient);
1111 } catch (UnregisteredRecipientException e) {
1112 return false;
1113 }
1114 return context.getContactHelper().isContactBlocked(recipientId);
1115 }
1116
1117 @Override
1118 public void sendContacts() throws IOException {
1119 context.getSyncHelper().sendContacts();
1120 }
1121
1122 @Override
1123 public List<Recipient> getRecipients(
1124 boolean onlyContacts,
1125 Optional<Boolean> blocked,
1126 Collection<RecipientIdentifier.Single> recipients,
1127 Optional<String> name
1128 ) {
1129 final var recipientIds = recipients.stream().map(a -> {
1130 try {
1131 return context.getRecipientHelper().resolveRecipient(a);
1132 } catch (UnregisteredRecipientException e) {
1133 return null;
1134 }
1135 }).filter(Objects::nonNull).collect(Collectors.toSet());
1136 if (!recipients.isEmpty() && recipientIds.isEmpty()) {
1137 return List.of();
1138 }
1139 // refresh profiles of explicitly given recipients
1140 context.getProfileHelper().refreshRecipientProfiles(recipientIds);
1141 return account.getRecipientStore()
1142 .getRecipients(onlyContacts, blocked, recipientIds, name)
1143 .stream()
1144 .map(s -> new Recipient(s.getRecipientId(),
1145 s.getAddress().toApiRecipientAddress(),
1146 s.getContact(),
1147 s.getProfileKey(),
1148 s.getExpiringProfileKeyCredential(),
1149 s.getProfile()))
1150 .toList();
1151 }
1152
1153 @Override
1154 public String getContactOrProfileName(RecipientIdentifier.Single recipient) {
1155 final RecipientId recipientId;
1156 try {
1157 recipientId = context.getRecipientHelper().resolveRecipient(recipient);
1158 } catch (UnregisteredRecipientException e) {
1159 return null;
1160 }
1161
1162 final var contact = account.getContactStore().getContact(recipientId);
1163 if (contact != null && !Util.isEmpty(contact.getName())) {
1164 return contact.getName();
1165 }
1166
1167 final var profile = context.getProfileHelper().getRecipientProfile(recipientId);
1168 if (profile != null) {
1169 return profile.getDisplayName();
1170 }
1171
1172 return null;
1173 }
1174
1175 @Override
1176 public Group getGroup(GroupId groupId) {
1177 return toGroup(context.getGroupHelper().getGroup(groupId));
1178 }
1179
1180 @Override
1181 public List<Identity> getIdentities() {
1182 return account.getIdentityKeyStore()
1183 .getIdentities()
1184 .stream()
1185 .map(this::toIdentity)
1186 .filter(Objects::nonNull)
1187 .toList();
1188 }
1189
1190 private Identity toIdentity(final IdentityInfo identityInfo) {
1191 if (identityInfo == null) {
1192 return null;
1193 }
1194
1195 final var address = account.getRecipientAddressResolver()
1196 .resolveRecipientAddress(account.getRecipientResolver().resolveRecipient(identityInfo.getServiceId()));
1197 if (address.serviceId().isPresent() && !Objects.equals(address.serviceId().get(),
1198 identityInfo.getServiceId())) {
1199 return null;
1200 }
1201 final var scannableFingerprint = context.getIdentityHelper()
1202 .computeSafetyNumberForScanning(identityInfo.getServiceId(), identityInfo.getIdentityKey());
1203 return new Identity(address.toApiRecipientAddress(),
1204 identityInfo.getIdentityKey(),
1205 context.getIdentityHelper()
1206 .computeSafetyNumber(identityInfo.getServiceId(), identityInfo.getIdentityKey()),
1207 scannableFingerprint == null ? null : scannableFingerprint.getSerialized(),
1208 identityInfo.getTrustLevel(),
1209 identityInfo.getDateAddedTimestamp());
1210 }
1211
1212 @Override
1213 public List<Identity> getIdentities(RecipientIdentifier.Single recipient) {
1214 ServiceId serviceId;
1215 try {
1216 final var address = account.getRecipientAddressResolver()
1217 .resolveRecipientAddress(context.getRecipientHelper().resolveRecipient(recipient));
1218 if (address.serviceId().isEmpty()) {
1219 return List.of();
1220 }
1221 serviceId = address.serviceId().get();
1222 } catch (UnregisteredRecipientException e) {
1223 return List.of();
1224 }
1225 final var identity = account.getIdentityKeyStore().getIdentityInfo(serviceId);
1226 return identity == null ? List.of() : List.of(toIdentity(identity));
1227 }
1228
1229 @Override
1230 public boolean trustIdentityVerified(
1231 RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode
1232 ) throws UnregisteredRecipientException {
1233 if (verificationCode instanceof IdentityVerificationCode.Fingerprint fingerprint) {
1234 return trustIdentity(recipient,
1235 r -> context.getIdentityHelper().trustIdentityVerified(r, fingerprint.fingerprint()));
1236 } else if (verificationCode instanceof IdentityVerificationCode.SafetyNumber safetyNumber) {
1237 return trustIdentity(recipient,
1238 r -> context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(r, safetyNumber.safetyNumber()));
1239 } else if (verificationCode instanceof IdentityVerificationCode.ScannableSafetyNumber safetyNumber) {
1240 return trustIdentity(recipient,
1241 r -> context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(r, safetyNumber.safetyNumber()));
1242 } else {
1243 throw new AssertionError("Invalid verification code type");
1244 }
1245 }
1246
1247 @Override
1248 public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
1249 return trustIdentity(recipient, r -> context.getIdentityHelper().trustIdentityAllKeys(r));
1250 }
1251
1252 private boolean trustIdentity(
1253 RecipientIdentifier.Single recipient, Function<RecipientId, Boolean> trustMethod
1254 ) throws UnregisteredRecipientException {
1255 final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
1256 final var updated = trustMethod.apply(recipientId);
1257 if (updated && this.isReceiving()) {
1258 context.getReceiveHelper().setNeedsToRetryFailedMessages(true);
1259 }
1260 return updated;
1261 }
1262
1263 @Override
1264 public void addAddressChangedListener(final Runnable listener) {
1265 synchronized (addressChangedListeners) {
1266 addressChangedListeners.add(listener);
1267 }
1268 }
1269
1270 @Override
1271 public void addClosedListener(final Runnable listener) {
1272 synchronized (closedListeners) {
1273 closedListeners.add(listener);
1274 }
1275 }
1276
1277 @Override
1278 public InputStream retrieveAttachment(final String id) throws IOException {
1279 return context.getAttachmentHelper().retrieveAttachment(id).getStream();
1280 }
1281
1282 @Override
1283 public void close() {
1284 Thread thread;
1285 synchronized (messageHandlers) {
1286 weakHandlers.clear();
1287 messageHandlers.clear();
1288 thread = receiveThread;
1289 receiveThread = null;
1290 }
1291 if (thread != null) {
1292 stopReceiveThread(thread);
1293 }
1294 executor.shutdown();
1295
1296 dependencies.getSignalWebSocket().disconnect();
1297 disposable.dispose();
1298
1299 if (account != null) {
1300 account.close();
1301 }
1302
1303 synchronized (closedListeners) {
1304 closedListeners.forEach(Runnable::run);
1305 closedListeners.clear();
1306 }
1307
1308 account = null;
1309 }
1310 }