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