]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
Rotate profile key after blocking a contact/group
[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 final var selfRecipientId = account.getSelfRecipientId();
698 boolean shouldRotateProfileKey = false;
699 for (final var recipientId : recipientIds) {
700 if (context.getContactHelper().isContactBlocked(recipientId) == blocked) {
701 continue;
702 }
703 context.getContactHelper().setContactBlocked(recipientId, blocked);
704 // if we don't have a common group with the blocked contact we need to rotate the profile key
705 shouldRotateProfileKey = blocked && (
706 shouldRotateProfileKey || account.getGroupStore()
707 .getGroups()
708 .stream()
709 .noneMatch(g -> g.isMember(selfRecipientId) && g.isMember(recipientId))
710 );
711 }
712 if (shouldRotateProfileKey) {
713 context.getProfileHelper().rotateProfileKey();
714 }
715 context.getSyncHelper().sendBlockedList();
716 }
717
718 @Override
719 public void setGroupsBlocked(
720 final Collection<GroupId> groupIds, final boolean blocked
721 ) throws GroupNotFoundException, NotMasterDeviceException, IOException {
722 if (!account.isMasterDevice()) {
723 throw new NotMasterDeviceException();
724 }
725 if (groupIds.size() == 0) {
726 return;
727 }
728 boolean shouldRotateProfileKey = false;
729 for (final var groupId : groupIds) {
730 if (context.getGroupHelper().isGroupBlocked(groupId) == blocked) {
731 continue;
732 }
733 context.getGroupHelper().setGroupBlocked(groupId, blocked);
734 shouldRotateProfileKey = blocked;
735 }
736 if (shouldRotateProfileKey) {
737 context.getProfileHelper().rotateProfileKey();
738 }
739 context.getSyncHelper().sendBlockedList();
740 }
741
742 @Override
743 public void setExpirationTimer(
744 RecipientIdentifier.Single recipient, int messageExpirationTimer
745 ) throws IOException, UnregisteredRecipientException {
746 var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
747 context.getContactHelper().setExpirationTimer(recipientId, messageExpirationTimer);
748 final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
749 try {
750 sendMessage(messageBuilder, Set.of(recipient));
751 } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) {
752 throw new AssertionError(e);
753 }
754 }
755
756 @Override
757 public StickerPackUrl uploadStickerPack(File path) throws IOException, StickerPackInvalidException {
758 var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path);
759
760 var messageSender = dependencies.getMessageSender();
761
762 var packKey = KeyUtils.createStickerUploadKey();
763 var packIdString = messageSender.uploadStickerManifest(manifest, packKey);
764 var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString));
765
766 var sticker = new Sticker(packId, packKey);
767 account.getStickerStore().updateSticker(sticker);
768
769 return new StickerPackUrl(packId, packKey);
770 }
771
772 @Override
773 public List<StickerPack> getStickerPacks() {
774 final var stickerPackStore = context.getStickerPackStore();
775 return account.getStickerStore().getStickerPacks().stream().map(pack -> {
776 if (stickerPackStore.existsStickerPack(pack.getPackId())) {
777 try {
778 final var manifest = stickerPackStore.retrieveManifest(pack.getPackId());
779 return new StickerPack(pack.getPackId(),
780 new StickerPackUrl(pack.getPackId(), pack.getPackKey()),
781 pack.isInstalled(),
782 manifest.title(),
783 manifest.author(),
784 Optional.ofNullable(manifest.cover() == null ? null : manifest.cover().toApi()),
785 manifest.stickers().stream().map(JsonStickerPack.JsonSticker::toApi).toList());
786 } catch (Exception e) {
787 logger.warn("Failed to read local sticker pack manifest: {}", e.getMessage(), e);
788 }
789 }
790
791 return new StickerPack(pack.getPackId(), pack.getPackKey(), pack.isInstalled());
792 }).toList();
793 }
794
795 @Override
796 public void requestAllSyncData() throws IOException {
797 context.getSyncHelper().requestAllSyncData();
798 retrieveRemoteStorage();
799 }
800
801 void retrieveRemoteStorage() throws IOException {
802 if (account.getStorageKey() != null) {
803 context.getStorageHelper().readDataFromStorage();
804 }
805 }
806
807 @Override
808 public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
809 if (isReceivingSynchronous) {
810 throw new IllegalStateException("Already receiving message synchronously.");
811 }
812 synchronized (messageHandlers) {
813 if (isWeakListener) {
814 weakHandlers.add(handler);
815 } else {
816 messageHandlers.add(handler);
817 startReceiveThreadIfRequired();
818 }
819 }
820 }
821
822 private static final AtomicInteger threadNumber = new AtomicInteger(0);
823
824 private void startReceiveThreadIfRequired() {
825 if (receiveThread != null) {
826 return;
827 }
828 receiveThread = new Thread(() -> {
829 logger.debug("Starting receiving messages");
830 context.getReceiveHelper().receiveMessagesContinuously((envelope, e) -> {
831 synchronized (messageHandlers) {
832 Stream.concat(messageHandlers.stream(), weakHandlers.stream()).forEach(h -> {
833 try {
834 h.handleMessage(envelope, e);
835 } catch (Throwable ex) {
836 logger.warn("Message handler failed, ignoring", ex);
837 }
838 });
839 }
840 });
841 logger.debug("Finished receiving messages");
842 synchronized (messageHandlers) {
843 receiveThread = null;
844
845 // Check if in the meantime another handler has been registered
846 if (!messageHandlers.isEmpty()) {
847 logger.debug("Another handler has been registered, starting receive thread again");
848 startReceiveThreadIfRequired();
849 }
850 }
851 });
852 receiveThread.setName("receive-" + threadNumber.getAndIncrement());
853
854 receiveThread.start();
855 }
856
857 @Override
858 public void removeReceiveHandler(final ReceiveMessageHandler handler) {
859 final Thread thread;
860 synchronized (messageHandlers) {
861 weakHandlers.remove(handler);
862 messageHandlers.remove(handler);
863 if (!messageHandlers.isEmpty() || receiveThread == null || isReceivingSynchronous) {
864 return;
865 }
866 thread = receiveThread;
867 receiveThread = null;
868 }
869
870 stopReceiveThread(thread);
871 }
872
873 private void stopReceiveThread(final Thread thread) {
874 if (context.getReceiveHelper().requestStopReceiveMessages()) {
875 logger.debug("Receive stop requested, interrupting read from server.");
876 thread.interrupt();
877 }
878 try {
879 thread.join();
880 } catch (InterruptedException ignored) {
881 }
882 }
883
884 @Override
885 public boolean isReceiving() {
886 if (isReceivingSynchronous) {
887 return true;
888 }
889 synchronized (messageHandlers) {
890 return messageHandlers.size() > 0;
891 }
892 }
893
894 @Override
895 public void receiveMessages(Duration timeout, ReceiveMessageHandler handler) throws IOException {
896 receiveMessages(timeout, true, handler);
897 }
898
899 @Override
900 public void receiveMessages(ReceiveMessageHandler handler) throws IOException {
901 receiveMessages(Duration.ofMinutes(1), false, handler);
902 }
903
904 private void receiveMessages(
905 Duration timeout, boolean returnOnTimeout, ReceiveMessageHandler handler
906 ) throws IOException {
907 if (isReceiving()) {
908 throw new IllegalStateException("Already receiving message.");
909 }
910 isReceivingSynchronous = true;
911 receiveThread = Thread.currentThread();
912 try {
913 context.getReceiveHelper().receiveMessages(timeout, returnOnTimeout, handler);
914 } finally {
915 receiveThread = null;
916 isReceivingSynchronous = false;
917 }
918 }
919
920 @Override
921 public void setIgnoreAttachments(final boolean ignoreAttachments) {
922 context.getReceiveHelper().setIgnoreAttachments(ignoreAttachments);
923 }
924
925 @Override
926 public boolean hasCaughtUpWithOldMessages() {
927 return context.getReceiveHelper().hasCaughtUpWithOldMessages();
928 }
929
930 @Override
931 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
932 final RecipientId recipientId;
933 try {
934 recipientId = context.getRecipientHelper().resolveRecipient(recipient);
935 } catch (IOException | UnregisteredRecipientException e) {
936 return false;
937 }
938 return context.getContactHelper().isContactBlocked(recipientId);
939 }
940
941 @Override
942 public void sendContacts() throws IOException {
943 context.getSyncHelper().sendContacts();
944 }
945
946 @Override
947 public List<Pair<RecipientAddress, Contact>> getContacts() {
948 return account.getContactStore()
949 .getContacts()
950 .stream()
951 .map(p -> new Pair<>(account.getRecipientStore().resolveRecipientAddress(p.first()), p.second()))
952 .toList();
953 }
954
955 @Override
956 public String getContactOrProfileName(RecipientIdentifier.Single recipient) {
957 final RecipientId recipientId;
958 try {
959 recipientId = context.getRecipientHelper().resolveRecipient(recipient);
960 } catch (IOException | UnregisteredRecipientException e) {
961 return null;
962 }
963
964 final var contact = account.getContactStore().getContact(recipientId);
965 if (contact != null && !Util.isEmpty(contact.getName())) {
966 return contact.getName();
967 }
968
969 final var profile = context.getProfileHelper().getRecipientProfile(recipientId);
970 if (profile != null) {
971 return profile.getDisplayName();
972 }
973
974 return null;
975 }
976
977 @Override
978 public Group getGroup(GroupId groupId) {
979 return toGroup(context.getGroupHelper().getGroup(groupId));
980 }
981
982 @Override
983 public List<Identity> getIdentities() {
984 return account.getIdentityKeyStore().getIdentities().stream().map(this::toIdentity).toList();
985 }
986
987 private Identity toIdentity(final IdentityInfo identityInfo) {
988 if (identityInfo == null) {
989 return null;
990 }
991
992 final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId());
993 final var scannableFingerprint = context.getIdentityHelper()
994 .computeSafetyNumberForScanning(identityInfo.getRecipientId(), identityInfo.getIdentityKey());
995 return new Identity(address,
996 identityInfo.getIdentityKey(),
997 context.getIdentityHelper()
998 .computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()),
999 scannableFingerprint == null ? null : scannableFingerprint.getSerialized(),
1000 identityInfo.getTrustLevel(),
1001 identityInfo.getDateAdded());
1002 }
1003
1004 @Override
1005 public List<Identity> getIdentities(RecipientIdentifier.Single recipient) {
1006 IdentityInfo identity;
1007 try {
1008 identity = account.getIdentityKeyStore()
1009 .getIdentity(context.getRecipientHelper().resolveRecipient(recipient));
1010 } catch (IOException | UnregisteredRecipientException e) {
1011 identity = null;
1012 }
1013 return identity == null ? List.of() : List.of(toIdentity(identity));
1014 }
1015
1016 @Override
1017 public boolean trustIdentityVerified(
1018 RecipientIdentifier.Single recipient, byte[] fingerprint
1019 ) throws UnregisteredRecipientException {
1020 return trustIdentity(recipient, r -> context.getIdentityHelper().trustIdentityVerified(r, fingerprint));
1021 }
1022
1023 @Override
1024 public boolean trustIdentityVerifiedSafetyNumber(
1025 RecipientIdentifier.Single recipient, String safetyNumber
1026 ) throws UnregisteredRecipientException {
1027 return trustIdentity(recipient,
1028 r -> context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(r, safetyNumber));
1029 }
1030
1031 @Override
1032 public boolean trustIdentityVerifiedSafetyNumber(
1033 RecipientIdentifier.Single recipient, byte[] safetyNumber
1034 ) throws UnregisteredRecipientException {
1035 return trustIdentity(recipient,
1036 r -> context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(r, safetyNumber));
1037 }
1038
1039 @Override
1040 public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
1041 return trustIdentity(recipient, r -> context.getIdentityHelper().trustIdentityAllKeys(r));
1042 }
1043
1044 private boolean trustIdentity(
1045 RecipientIdentifier.Single recipient, Function<RecipientId, Boolean> trustMethod
1046 ) throws UnregisteredRecipientException {
1047 RecipientId recipientId;
1048 try {
1049 recipientId = context.getRecipientHelper().resolveRecipient(recipient);
1050 } catch (IOException e) {
1051 return false;
1052 }
1053 final var updated = trustMethod.apply(recipientId);
1054 if (updated && this.isReceiving()) {
1055 context.getReceiveHelper().setNeedsToRetryFailedMessages(true);
1056 }
1057 return updated;
1058 }
1059
1060 @Override
1061 public void addAddressChangedListener(final Runnable listener) {
1062 synchronized (addressChangedListeners) {
1063 addressChangedListeners.add(listener);
1064 }
1065 }
1066
1067 @Override
1068 public void addClosedListener(final Runnable listener) {
1069 synchronized (closedListeners) {
1070 closedListeners.add(listener);
1071 }
1072 }
1073
1074 @Override
1075 public void close() {
1076 Thread thread;
1077 synchronized (messageHandlers) {
1078 weakHandlers.clear();
1079 messageHandlers.clear();
1080 thread = receiveThread;
1081 receiveThread = null;
1082 }
1083 if (thread != null) {
1084 stopReceiveThread(thread);
1085 }
1086 executor.shutdown();
1087
1088 dependencies.getSignalWebSocket().disconnect();
1089 disposable.dispose();
1090
1091 if (account != null) {
1092 account.close();
1093 }
1094
1095 synchronized (closedListeners) {
1096 closedListeners.forEach(Runnable::run);
1097 closedListeners.clear();
1098 }
1099
1100 account = null;
1101 }
1102 }