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