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