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