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