]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
1f731ee897717dbc75c050a5afa2460859db2afc
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / ManagerImpl.java
1 /*
2 Copyright (C) 2015-2021 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.Message;
26 import org.asamk.signal.manager.api.Pair;
27 import org.asamk.signal.manager.api.RecipientIdentifier;
28 import org.asamk.signal.manager.api.SendGroupMessageResults;
29 import org.asamk.signal.manager.api.SendMessageResult;
30 import org.asamk.signal.manager.api.SendMessageResults;
31 import org.asamk.signal.manager.api.TypingAction;
32 import org.asamk.signal.manager.api.UnregisteredRecipientException;
33 import org.asamk.signal.manager.api.UpdateGroup;
34 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
35 import org.asamk.signal.manager.groups.GroupId;
36 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
37 import org.asamk.signal.manager.groups.GroupNotFoundException;
38 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
39 import org.asamk.signal.manager.groups.LastGroupAdminException;
40 import org.asamk.signal.manager.groups.NotAGroupMemberException;
41 import org.asamk.signal.manager.helper.Context;
42 import org.asamk.signal.manager.storage.SignalAccount;
43 import org.asamk.signal.manager.storage.groups.GroupInfo;
44 import org.asamk.signal.manager.storage.identities.IdentityInfo;
45 import org.asamk.signal.manager.storage.recipients.Contact;
46 import org.asamk.signal.manager.storage.recipients.Profile;
47 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
48 import org.asamk.signal.manager.storage.recipients.RecipientId;
49 import org.asamk.signal.manager.storage.stickers.Sticker;
50 import org.asamk.signal.manager.storage.stickers.StickerPackId;
51 import org.asamk.signal.manager.util.KeyUtils;
52 import org.asamk.signal.manager.util.StickerUtils;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55 import org.whispersystems.libsignal.InvalidKeyException;
56 import org.whispersystems.libsignal.ecc.ECPublicKey;
57 import org.whispersystems.libsignal.util.guava.Optional;
58 import org.whispersystems.signalservice.api.SignalSessionLock;
59 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
60 import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
61 import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
62 import org.whispersystems.signalservice.api.push.ACI;
63 import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
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.net.URISyntaxException;
75 import java.net.URLEncoder;
76 import java.nio.charset.StandardCharsets;
77 import java.time.Duration;
78 import java.util.ArrayList;
79 import java.util.HashMap;
80 import java.util.HashSet;
81 import java.util.List;
82 import java.util.Map;
83 import java.util.Set;
84 import java.util.UUID;
85 import java.util.concurrent.ExecutorService;
86 import java.util.concurrent.Executors;
87 import java.util.concurrent.TimeUnit;
88 import java.util.concurrent.locks.ReentrantLock;
89 import java.util.stream.Collectors;
90 import java.util.stream.Stream;
91
92 import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
93
94 public class ManagerImpl implements Manager {
95
96 private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class);
97
98 private final SignalDependencies dependencies;
99
100 private SignalAccount account;
101
102 private final ExecutorService executor = Executors.newCachedThreadPool();
103
104 private final Context context;
105
106 private Thread receiveThread;
107 private final Set<ReceiveMessageHandler> weakHandlers = new HashSet<>();
108 private final Set<ReceiveMessageHandler> messageHandlers = new HashSet<>();
109 private final List<Runnable> closedListeners = new ArrayList<>();
110 private boolean isReceivingSynchronous;
111
112 ManagerImpl(
113 SignalAccount account,
114 PathConfig pathConfig,
115 ServiceEnvironmentConfig serviceEnvironmentConfig,
116 String userAgent
117 ) {
118 this.account = account;
119
120 final var credentialsProvider = new DynamicCredentialsProvider(account.getAci(),
121 account.getAccount(),
122 account.getPassword(),
123 account.getDeviceId());
124 final var sessionLock = new SignalSessionLock() {
125 private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
126
127 @Override
128 public Lock acquire() {
129 LEGACY_LOCK.lock();
130 return LEGACY_LOCK::unlock;
131 }
132 };
133 this.dependencies = new SignalDependencies(serviceEnvironmentConfig,
134 userAgent,
135 credentialsProvider,
136 account.getSignalProtocolStore(),
137 executor,
138 sessionLock);
139 final var avatarStore = new AvatarStore(pathConfig.avatarsPath());
140 final var attachmentStore = new AttachmentStore(pathConfig.attachmentsPath());
141 final var stickerPackStore = new StickerPackStore(pathConfig.stickerPacksPath());
142
143 this.context = new Context(account, dependencies, avatarStore, attachmentStore, stickerPackStore);
144 this.context.getReceiveHelper().setAuthenticationFailureListener(() -> {
145 try {
146 close();
147 } catch (IOException e) {
148 logger.warn("Failed to close account after authentication failure", e);
149 }
150 });
151 this.context.getReceiveHelper().setCaughtUpWithOldMessagesListener(() -> {
152 synchronized (this) {
153 this.notifyAll();
154 }
155 });
156 }
157
158 @Override
159 public String getSelfNumber() {
160 return account.getAccount();
161 }
162
163 @Override
164 public void checkAccountState() throws IOException {
165 if (account.getLastReceiveTimestamp() == 0) {
166 logger.info("The Signal protocol expects that incoming messages are regularly received.");
167 } else {
168 var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp();
169 long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS);
170 if (days > 7) {
171 logger.warn(
172 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
173 days);
174 }
175 }
176 try {
177 context.getPreKeyHelper().refreshPreKeysIfNecessary();
178 if (account.getAci() == null) {
179 account.setAci(ACI.parseOrNull(dependencies.getAccountManager().getWhoAmI().getAci()));
180 }
181 updateAccountAttributes(null);
182 } catch (AuthorizationFailedException e) {
183 account.setRegistered(false);
184 throw e;
185 }
186 }
187
188 /**
189 * This is used for checking a set of phone numbers for registration on Signal
190 *
191 * @param numbers The set of phone number in question
192 * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null.
193 * @throws IOException if it's unable to get the contacts to check if they're registered
194 */
195 @Override
196 public Map<String, Pair<String, UUID>> areUsersRegistered(Set<String> numbers) throws IOException {
197 final var canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> {
198 try {
199 final var canonicalizedNumber = PhoneNumberFormatter.formatNumber(n, account.getAccount());
200 if (!canonicalizedNumber.equals(n)) {
201 logger.debug("Normalized number {} to {}.", n, canonicalizedNumber);
202 }
203 return canonicalizedNumber;
204 } catch (InvalidNumberException e) {
205 return "";
206 }
207 }));
208
209 // Note "registeredUsers" has no optionals. It only gives us info on users who are registered
210 final var canonicalizedNumbersSet = canonicalizedNumbers.values()
211 .stream()
212 .filter(s -> !s.isEmpty())
213 .collect(Collectors.toSet());
214 final var registeredUsers = context.getRecipientHelper().getRegisteredUsers(canonicalizedNumbersSet);
215
216 return numbers.stream().collect(Collectors.toMap(n -> n, n -> {
217 final var number = canonicalizedNumbers.get(n);
218 final var aci = registeredUsers.get(number);
219 return new Pair<>(number.isEmpty() ? null : number, aci == null ? null : aci.uuid());
220 }));
221 }
222
223 @Override
224 public void updateAccountAttributes(String deviceName) throws IOException {
225 final String encryptedDeviceName;
226 if (deviceName == null) {
227 encryptedDeviceName = account.getEncryptedDeviceName();
228 } else {
229 final var privateKey = account.getIdentityKeyPair().getPrivateKey();
230 encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
231 account.setEncryptedDeviceName(encryptedDeviceName);
232 }
233 dependencies.getAccountManager()
234 .setAccountAttributes(encryptedDeviceName,
235 null,
236 account.getLocalRegistrationId(),
237 true,
238 null,
239 account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
240 account.getSelfUnidentifiedAccessKey(),
241 account.isUnrestrictedUnidentifiedAccess(),
242 capabilities,
243 account.isDiscoverableByPhoneNumber());
244 }
245
246 @Override
247 public Configuration getConfiguration() {
248 final var configurationStore = account.getConfigurationStore();
249 return new Configuration(java.util.Optional.ofNullable(configurationStore.getReadReceipts()),
250 java.util.Optional.ofNullable(configurationStore.getUnidentifiedDeliveryIndicators()),
251 java.util.Optional.ofNullable(configurationStore.getTypingIndicators()),
252 java.util.Optional.ofNullable(configurationStore.getLinkPreviews()));
253 }
254
255 @Override
256 public void updateConfiguration(
257 Configuration configuration
258 ) throws NotMasterDeviceException {
259 if (!account.isMasterDevice()) {
260 throw new NotMasterDeviceException();
261 }
262
263 final var configurationStore = account.getConfigurationStore();
264 if (configuration.readReceipts().isPresent()) {
265 configurationStore.setReadReceipts(configuration.readReceipts().get());
266 }
267 if (configuration.unidentifiedDeliveryIndicators().isPresent()) {
268 configurationStore.setUnidentifiedDeliveryIndicators(configuration.unidentifiedDeliveryIndicators().get());
269 }
270 if (configuration.typingIndicators().isPresent()) {
271 configurationStore.setTypingIndicators(configuration.typingIndicators().get());
272 }
273 if (configuration.linkPreviews().isPresent()) {
274 configurationStore.setLinkPreviews(configuration.linkPreviews().get());
275 }
276 context.getSyncHelper().sendConfigurationMessage();
277 }
278
279 /**
280 * @param givenName if null, the previous givenName will be kept
281 * @param familyName if null, the previous familyName will be kept
282 * @param about if null, the previous about text will be kept
283 * @param aboutEmoji if null, the previous about emoji will be kept
284 * @param avatar if avatar is null the image from the local avatar store is used (if present),
285 */
286 @Override
287 public void setProfile(
288 String givenName, final String familyName, String about, String aboutEmoji, java.util.Optional<File> avatar
289 ) throws IOException {
290 context.getProfileHelper()
291 .setProfile(givenName,
292 familyName,
293 about,
294 aboutEmoji,
295 avatar == null ? null : Optional.fromNullable(avatar.orElse(null)));
296 context.getSyncHelper().sendSyncFetchProfileMessage();
297 }
298
299 @Override
300 public void unregister() throws IOException {
301 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
302 // If this is the master device, other users can't send messages to this number anymore.
303 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
304 dependencies.getAccountManager().setGcmId(Optional.absent());
305
306 account.setRegistered(false);
307 close();
308 }
309
310 @Override
311 public void deleteAccount() throws IOException {
312 try {
313 context.getPinHelper().removeRegistrationLockPin();
314 } catch (IOException e) {
315 logger.warn("Failed to remove registration lock pin");
316 }
317 account.setRegistrationLockPin(null, null);
318
319 dependencies.getAccountManager().deleteAccount();
320
321 account.setRegistered(false);
322 close();
323 }
324
325 @Override
326 public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException {
327 captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
328
329 dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
330 }
331
332 @Override
333 public List<Device> getLinkedDevices() throws IOException {
334 var devices = dependencies.getAccountManager().getDevices();
335 account.setMultiDevice(devices.size() > 1);
336 var identityKey = account.getIdentityKeyPair().getPrivateKey();
337 return devices.stream().map(d -> {
338 String deviceName = d.getName();
339 if (deviceName != null) {
340 try {
341 deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey);
342 } catch (IOException e) {
343 logger.debug("Failed to decrypt device name, maybe plain text?", e);
344 }
345 }
346 return new Device(d.getId(),
347 deviceName,
348 d.getCreated(),
349 d.getLastSeen(),
350 d.getId() == account.getDeviceId());
351 }).toList();
352 }
353
354 @Override
355 public void removeLinkedDevices(long deviceId) throws IOException {
356 dependencies.getAccountManager().removeDevice(deviceId);
357 var devices = dependencies.getAccountManager().getDevices();
358 account.setMultiDevice(devices.size() > 1);
359 }
360
361 @Override
362 public void addDeviceLink(URI linkUri) throws IOException, InvalidDeviceLinkException {
363 var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri);
364
365 addDevice(info.deviceIdentifier(), info.deviceKey());
366 }
367
368 private void addDevice(
369 String deviceIdentifier, ECPublicKey deviceKey
370 ) throws IOException, InvalidDeviceLinkException {
371 var identityKeyPair = account.getIdentityKeyPair();
372 var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
373
374 try {
375 dependencies.getAccountManager()
376 .addDevice(deviceIdentifier,
377 deviceKey,
378 identityKeyPair,
379 Optional.of(account.getProfileKey().serialize()),
380 verificationCode);
381 } catch (InvalidKeyException e) {
382 throw new InvalidDeviceLinkException("Invalid device link", e);
383 }
384 account.setMultiDevice(true);
385 }
386
387 @Override
388 public void setRegistrationLockPin(java.util.Optional<String> pin) throws IOException {
389 if (!account.isMasterDevice()) {
390 throw new RuntimeException("Only master device can set a PIN");
391 }
392 if (pin.isPresent()) {
393 final var masterKey = account.getPinMasterKey() != null
394 ? account.getPinMasterKey()
395 : KeyUtils.createMasterKey();
396
397 context.getPinHelper().setRegistrationLockPin(pin.get(), masterKey);
398
399 account.setRegistrationLockPin(pin.get(), masterKey);
400 } else {
401 // Remove KBS Pin
402 context.getPinHelper().removeRegistrationLockPin();
403
404 account.setRegistrationLockPin(null, null);
405 }
406 }
407
408 void refreshPreKeys() throws IOException {
409 context.getPreKeyHelper().refreshPreKeys();
410 }
411
412 @Override
413 public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
414 return context.getProfileHelper().getRecipientProfile(context.getRecipientHelper().resolveRecipient(recipient));
415 }
416
417 @Override
418 public List<Group> getGroups() {
419 return account.getGroupStore().getGroups().stream().map(this::toGroup).toList();
420 }
421
422 private Group toGroup(final GroupInfo groupInfo) {
423 if (groupInfo == null) {
424 return null;
425 }
426
427 return new Group(groupInfo.getGroupId(),
428 groupInfo.getTitle(),
429 groupInfo.getDescription(),
430 groupInfo.getGroupInviteLink(),
431 groupInfo.getMembers()
432 .stream()
433 .map(account.getRecipientStore()::resolveRecipientAddress)
434 .collect(Collectors.toSet()),
435 groupInfo.getPendingMembers()
436 .stream()
437 .map(account.getRecipientStore()::resolveRecipientAddress)
438 .collect(Collectors.toSet()),
439 groupInfo.getRequestingMembers()
440 .stream()
441 .map(account.getRecipientStore()::resolveRecipientAddress)
442 .collect(Collectors.toSet()),
443 groupInfo.getAdminMembers()
444 .stream()
445 .map(account.getRecipientStore()::resolveRecipientAddress)
446 .collect(Collectors.toSet()),
447 groupInfo.isBlocked(),
448 groupInfo.getMessageExpirationTimer(),
449 groupInfo.getPermissionAddMember(),
450 groupInfo.getPermissionEditDetails(),
451 groupInfo.getPermissionSendMessage(),
452 groupInfo.isMember(account.getSelfRecipientId()),
453 groupInfo.isAdmin(account.getSelfRecipientId()));
454 }
455
456 @Override
457 public SendGroupMessageResults quitGroup(
458 GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins
459 ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException {
460 final var newAdmins = context.getRecipientHelper().resolveRecipients(groupAdmins);
461 return context.getGroupHelper().quitGroup(groupId, newAdmins);
462 }
463
464 @Override
465 public void deleteGroup(GroupId groupId) throws IOException {
466 context.getGroupHelper().deleteGroup(groupId);
467 }
468
469 @Override
470 public Pair<GroupId, SendGroupMessageResults> createGroup(
471 String name, Set<RecipientIdentifier.Single> members, File avatarFile
472 ) throws IOException, AttachmentInvalidException, UnregisteredRecipientException {
473 return context.getGroupHelper()
474 .createGroup(name,
475 members == null ? null : context.getRecipientHelper().resolveRecipients(members),
476 avatarFile);
477 }
478
479 @Override
480 public SendGroupMessageResults updateGroup(
481 final GroupId groupId, final UpdateGroup updateGroup
482 ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException {
483 return context.getGroupHelper()
484 .updateGroup(groupId,
485 updateGroup.getName(),
486 updateGroup.getDescription(),
487 updateGroup.getMembers() == null
488 ? null
489 : context.getRecipientHelper().resolveRecipients(updateGroup.getMembers()),
490 updateGroup.getRemoveMembers() == null
491 ? null
492 : context.getRecipientHelper().resolveRecipients(updateGroup.getRemoveMembers()),
493 updateGroup.getAdmins() == null
494 ? null
495 : context.getRecipientHelper().resolveRecipients(updateGroup.getAdmins()),
496 updateGroup.getRemoveAdmins() == null
497 ? null
498 : context.getRecipientHelper().resolveRecipients(updateGroup.getRemoveAdmins()),
499 updateGroup.isResetGroupLink(),
500 updateGroup.getGroupLinkState(),
501 updateGroup.getAddMemberPermission(),
502 updateGroup.getEditDetailsPermission(),
503 updateGroup.getAvatarFile(),
504 updateGroup.getExpirationTimer(),
505 updateGroup.getIsAnnouncementGroup());
506 }
507
508 @Override
509 public Pair<GroupId, SendGroupMessageResults> joinGroup(
510 GroupInviteLinkUrl inviteLinkUrl
511 ) throws IOException, InactiveGroupLinkException {
512 return context.getGroupHelper().joinGroup(inviteLinkUrl);
513 }
514
515 private SendMessageResults sendMessage(
516 SignalServiceDataMessage.Builder messageBuilder, Set<RecipientIdentifier> recipients
517 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
518 var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
519 long timestamp = System.currentTimeMillis();
520 messageBuilder.withTimestamp(timestamp);
521 for (final var recipient : recipients) {
522 if (recipient instanceof RecipientIdentifier.Single single) {
523 try {
524 final var recipientId = context.getRecipientHelper().resolveRecipient(single);
525 final var result = context.getSendHelper().sendMessage(messageBuilder, recipientId);
526 results.put(recipient,
527 List.of(SendMessageResult.from(result,
528 account.getRecipientStore(),
529 account.getRecipientStore()::resolveRecipientAddress)));
530 } catch (UnregisteredRecipientException e) {
531 results.put(recipient,
532 List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress())));
533 }
534 } else if (recipient instanceof RecipientIdentifier.NoteToSelf) {
535 final var result = context.getSendHelper().sendSelfMessage(messageBuilder);
536 results.put(recipient,
537 List.of(SendMessageResult.from(result,
538 account.getRecipientStore(),
539 account.getRecipientStore()::resolveRecipientAddress)));
540 } else if (recipient instanceof RecipientIdentifier.Group group) {
541 final var result = context.getSendHelper().sendAsGroupMessage(messageBuilder, group.groupId());
542 results.put(recipient,
543 result.stream()
544 .map(sendMessageResult -> SendMessageResult.from(sendMessageResult,
545 account.getRecipientStore(),
546 account.getRecipientStore()::resolveRecipientAddress))
547 .toList());
548 }
549 }
550 return new SendMessageResults(timestamp, results);
551 }
552
553 private SendMessageResults sendTypingMessage(
554 SignalServiceTypingMessage.Action action, Set<RecipientIdentifier> recipients
555 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
556 var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
557 final var timestamp = System.currentTimeMillis();
558 for (var recipient : recipients) {
559 if (recipient instanceof RecipientIdentifier.Single single) {
560 final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent());
561 try {
562 final var recipientId = context.getRecipientHelper().resolveRecipient(single);
563 final var result = context.getSendHelper().sendTypingMessage(message, recipientId);
564 results.put(recipient,
565 List.of(SendMessageResult.from(result,
566 account.getRecipientStore(),
567 account.getRecipientStore()::resolveRecipientAddress)));
568 } catch (UnregisteredRecipientException e) {
569 results.put(recipient,
570 List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress())));
571 }
572 } else if (recipient instanceof RecipientIdentifier.Group) {
573 final var groupId = ((RecipientIdentifier.Group) recipient).groupId();
574 final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize()));
575 final var result = context.getSendHelper().sendGroupTypingMessage(message, groupId);
576 results.put(recipient,
577 result.stream()
578 .map(r -> SendMessageResult.from(r,
579 account.getRecipientStore(),
580 account.getRecipientStore()::resolveRecipientAddress))
581 .toList());
582 }
583 }
584 return new SendMessageResults(timestamp, results);
585 }
586
587 @Override
588 public SendMessageResults sendTypingMessage(
589 TypingAction action, Set<RecipientIdentifier> recipients
590 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
591 return sendTypingMessage(action.toSignalService(), recipients);
592 }
593
594 @Override
595 public SendMessageResults sendReadReceipt(
596 RecipientIdentifier.Single sender, List<Long> messageIds
597 ) throws IOException {
598 final var timestamp = System.currentTimeMillis();
599 var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
600 messageIds,
601 timestamp);
602
603 return sendReceiptMessage(sender, timestamp, receiptMessage);
604 }
605
606 @Override
607 public SendMessageResults sendViewedReceipt(
608 RecipientIdentifier.Single sender, List<Long> messageIds
609 ) throws IOException {
610 final var timestamp = System.currentTimeMillis();
611 var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
612 messageIds,
613 timestamp);
614
615 return sendReceiptMessage(sender, timestamp, receiptMessage);
616 }
617
618 private SendMessageResults sendReceiptMessage(
619 final RecipientIdentifier.Single sender,
620 final long timestamp,
621 final SignalServiceReceiptMessage receiptMessage
622 ) throws IOException {
623 try {
624 final var result = context.getSendHelper()
625 .sendReceiptMessage(receiptMessage, context.getRecipientHelper().resolveRecipient(sender));
626 return new SendMessageResults(timestamp,
627 Map.of(sender,
628 List.of(SendMessageResult.from(result,
629 account.getRecipientStore(),
630 account.getRecipientStore()::resolveRecipientAddress))));
631 } catch (UnregisteredRecipientException e) {
632 return new SendMessageResults(timestamp,
633 Map.of(sender, List.of(SendMessageResult.unregisteredFailure(sender.toPartialRecipientAddress()))));
634 }
635 }
636
637 @Override
638 public SendMessageResults sendMessage(
639 Message message, Set<RecipientIdentifier> recipients
640 ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
641 final var messageBuilder = SignalServiceDataMessage.newBuilder();
642 applyMessage(messageBuilder, message);
643 return sendMessage(messageBuilder, recipients);
644 }
645
646 private void applyMessage(
647 final SignalServiceDataMessage.Builder messageBuilder, final Message message
648 ) throws AttachmentInvalidException, IOException, UnregisteredRecipientException {
649 messageBuilder.withBody(message.messageText());
650 final var attachments = message.attachments();
651 if (attachments != null) {
652 messageBuilder.withAttachments(context.getAttachmentHelper().uploadAttachments(attachments));
653 }
654 if (message.mentions().size() > 0) {
655 messageBuilder.withMentions(resolveMentions(message.mentions()));
656 }
657 if (message.quote().isPresent()) {
658 final var quote = message.quote().get();
659 messageBuilder.withQuote(new SignalServiceDataMessage.Quote(quote.timestamp(),
660 context.getRecipientHelper()
661 .resolveSignalServiceAddress(context.getRecipientHelper().resolveRecipient(quote.author())),
662 quote.message(),
663 List.of(),
664 resolveMentions(quote.mentions())));
665 }
666 }
667
668 private ArrayList<SignalServiceDataMessage.Mention> resolveMentions(final List<Message.Mention> mentionList) throws IOException, UnregisteredRecipientException {
669 final var mentions = new ArrayList<SignalServiceDataMessage.Mention>();
670 for (final var m : mentionList) {
671 final var recipientId = context.getRecipientHelper().resolveRecipient(m.recipient());
672 mentions.add(new SignalServiceDataMessage.Mention(context.getRecipientHelper()
673 .resolveSignalServiceAddress(recipientId)
674 .getAci(), m.start(), m.length()));
675 }
676 return mentions;
677 }
678
679 @Override
680 public SendMessageResults sendRemoteDeleteMessage(
681 long targetSentTimestamp, Set<RecipientIdentifier> recipients
682 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
683 var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
684 final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
685 return sendMessage(messageBuilder, recipients);
686 }
687
688 @Override
689 public SendMessageResults sendMessageReaction(
690 String emoji,
691 boolean remove,
692 RecipientIdentifier.Single targetAuthor,
693 long targetSentTimestamp,
694 Set<RecipientIdentifier> recipients
695 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
696 var targetAuthorRecipientId = context.getRecipientHelper().resolveRecipient(targetAuthor);
697 var reaction = new SignalServiceDataMessage.Reaction(emoji,
698 remove,
699 context.getRecipientHelper().resolveSignalServiceAddress(targetAuthorRecipientId),
700 targetSentTimestamp);
701 final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction);
702 return sendMessage(messageBuilder, recipients);
703 }
704
705 @Override
706 public SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException {
707 var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
708
709 try {
710 return sendMessage(messageBuilder,
711 recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()));
712 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
713 throw new AssertionError(e);
714 } finally {
715 for (var recipient : recipients) {
716 final RecipientId recipientId;
717 try {
718 recipientId = context.getRecipientHelper().resolveRecipient(recipient);
719 } catch (UnregisteredRecipientException e) {
720 continue;
721 }
722 account.getSessionStore().deleteAllSessions(recipientId);
723 }
724 }
725 }
726
727 @Override
728 public void deleteRecipient(final RecipientIdentifier.Single recipient) {
729 account.removeRecipient(account.getRecipientStore().resolveRecipient(recipient.toPartialRecipientAddress()));
730 }
731
732 @Override
733 public void deleteContact(final RecipientIdentifier.Single recipient) {
734 account.getContactStore()
735 .deleteContact(account.getRecipientStore().resolveRecipient(recipient.toPartialRecipientAddress()));
736 }
737
738 @Override
739 public void setContactName(
740 RecipientIdentifier.Single recipient, String name
741 ) throws NotMasterDeviceException, IOException, UnregisteredRecipientException {
742 if (!account.isMasterDevice()) {
743 throw new NotMasterDeviceException();
744 }
745 context.getContactHelper().setContactName(context.getRecipientHelper().resolveRecipient(recipient), name);
746 }
747
748 @Override
749 public void setContactBlocked(
750 RecipientIdentifier.Single recipient, boolean blocked
751 ) throws NotMasterDeviceException, IOException, UnregisteredRecipientException {
752 if (!account.isMasterDevice()) {
753 throw new NotMasterDeviceException();
754 }
755 context.getContactHelper().setContactBlocked(context.getRecipientHelper().resolveRecipient(recipient), blocked);
756 // TODO cycle our profile key, if we're not together in a group with recipient
757 context.getSyncHelper().sendBlockedList();
758 }
759
760 @Override
761 public void setGroupBlocked(
762 final GroupId groupId, final boolean blocked
763 ) throws GroupNotFoundException, NotMasterDeviceException {
764 if (!account.isMasterDevice()) {
765 throw new NotMasterDeviceException();
766 }
767 context.getGroupHelper().setGroupBlocked(groupId, blocked);
768 // TODO cycle our profile key
769 context.getSyncHelper().sendBlockedList();
770 }
771
772 /**
773 * Change the expiration timer for a contact
774 */
775 @Override
776 public void setExpirationTimer(
777 RecipientIdentifier.Single recipient, int messageExpirationTimer
778 ) throws IOException, UnregisteredRecipientException {
779 var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
780 context.getContactHelper().setExpirationTimer(recipientId, messageExpirationTimer);
781 final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
782 try {
783 sendMessage(messageBuilder, Set.of(recipient));
784 } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) {
785 throw new AssertionError(e);
786 }
787 }
788
789 /**
790 * Upload the sticker pack from path.
791 *
792 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
793 * @return if successful, returns the URL to install the sticker pack in the signal app
794 */
795 @Override
796 public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException {
797 var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path);
798
799 var messageSender = dependencies.getMessageSender();
800
801 var packKey = KeyUtils.createStickerUploadKey();
802 var packIdString = messageSender.uploadStickerManifest(manifest, packKey);
803 var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString));
804
805 var sticker = new Sticker(packId, packKey);
806 account.getStickerStore().updateSticker(sticker);
807
808 try {
809 return new URI("https",
810 "signal.art",
811 "/addstickers/",
812 "pack_id="
813 + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8)
814 + "&pack_key="
815 + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8));
816 } catch (URISyntaxException e) {
817 throw new AssertionError(e);
818 }
819 }
820
821 @Override
822 public void requestAllSyncData() throws IOException {
823 context.getSyncHelper().requestAllSyncData();
824 retrieveRemoteStorage();
825 }
826
827 void retrieveRemoteStorage() throws IOException {
828 if (account.getStorageKey() != null) {
829 context.getStorageHelper().readDataFromStorage();
830 }
831 }
832
833 @Override
834 public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
835 if (isReceivingSynchronous) {
836 throw new IllegalStateException("Already receiving message synchronously.");
837 }
838 synchronized (messageHandlers) {
839 if (isWeakListener) {
840 weakHandlers.add(handler);
841 } else {
842 messageHandlers.add(handler);
843 startReceiveThreadIfRequired();
844 }
845 }
846 }
847
848 private void startReceiveThreadIfRequired() {
849 if (receiveThread != null) {
850 return;
851 }
852 receiveThread = new Thread(() -> {
853 logger.debug("Starting receiving messages");
854 while (!Thread.interrupted()) {
855 try {
856 context.getReceiveHelper().receiveMessages(Duration.ofMinutes(1), false, (envelope, e) -> {
857 synchronized (messageHandlers) {
858 Stream.concat(messageHandlers.stream(), weakHandlers.stream()).forEach(h -> {
859 try {
860 h.handleMessage(envelope, e);
861 } catch (Exception ex) {
862 logger.warn("Message handler failed, ignoring", ex);
863 }
864 });
865 }
866 });
867 break;
868 } catch (IOException e) {
869 logger.warn("Receiving messages failed, retrying", e);
870 }
871 }
872 logger.debug("Finished receiving messages");
873 synchronized (messageHandlers) {
874 receiveThread = null;
875
876 // Check if in the meantime another handler has been registered
877 if (!messageHandlers.isEmpty()) {
878 logger.debug("Another handler has been registered, starting receive thread again");
879 startReceiveThreadIfRequired();
880 }
881 }
882 });
883
884 receiveThread.start();
885 }
886
887 @Override
888 public void removeReceiveHandler(final ReceiveMessageHandler handler) {
889 final Thread thread;
890 synchronized (messageHandlers) {
891 weakHandlers.remove(handler);
892 messageHandlers.remove(handler);
893 if (!messageHandlers.isEmpty() || receiveThread == null || isReceivingSynchronous) {
894 return;
895 }
896 thread = receiveThread;
897 receiveThread = null;
898 }
899
900 stopReceiveThread(thread);
901 }
902
903 private void stopReceiveThread(final Thread thread) {
904 thread.interrupt();
905 try {
906 thread.join();
907 } catch (InterruptedException ignored) {
908 }
909 }
910
911 @Override
912 public boolean isReceiving() {
913 if (isReceivingSynchronous) {
914 return true;
915 }
916 synchronized (messageHandlers) {
917 return messageHandlers.size() > 0;
918 }
919 }
920
921 @Override
922 public void receiveMessages(Duration timeout, ReceiveMessageHandler handler) throws IOException {
923 receiveMessages(timeout, true, handler);
924 }
925
926 @Override
927 public void receiveMessages(ReceiveMessageHandler handler) throws IOException {
928 receiveMessages(Duration.ofMinutes(1), false, handler);
929 }
930
931 private void receiveMessages(
932 Duration timeout, boolean returnOnTimeout, ReceiveMessageHandler handler
933 ) throws IOException {
934 if (isReceiving()) {
935 throw new IllegalStateException("Already receiving message.");
936 }
937 isReceivingSynchronous = true;
938 receiveThread = Thread.currentThread();
939 try {
940 context.getReceiveHelper().receiveMessages(timeout, returnOnTimeout, handler);
941 } finally {
942 receiveThread = null;
943 isReceivingSynchronous = false;
944 }
945 }
946
947 @Override
948 public void setIgnoreAttachments(final boolean ignoreAttachments) {
949 context.getReceiveHelper().setIgnoreAttachments(ignoreAttachments);
950 }
951
952 @Override
953 public boolean hasCaughtUpWithOldMessages() {
954 return context.getReceiveHelper().hasCaughtUpWithOldMessages();
955 }
956
957 @Override
958 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
959 final RecipientId recipientId;
960 try {
961 recipientId = context.getRecipientHelper().resolveRecipient(recipient);
962 } catch (IOException | UnregisteredRecipientException e) {
963 return false;
964 }
965 return context.getContactHelper().isContactBlocked(recipientId);
966 }
967
968 @Override
969 public void sendContacts() throws IOException {
970 context.getSyncHelper().sendContacts();
971 }
972
973 @Override
974 public List<Pair<RecipientAddress, Contact>> getContacts() {
975 return account.getContactStore()
976 .getContacts()
977 .stream()
978 .map(p -> new Pair<>(account.getRecipientStore().resolveRecipientAddress(p.first()), p.second()))
979 .toList();
980 }
981
982 @Override
983 public String getContactOrProfileName(RecipientIdentifier.Single recipient) {
984 final RecipientId recipientId;
985 try {
986 recipientId = context.getRecipientHelper().resolveRecipient(recipient);
987 } catch (IOException | UnregisteredRecipientException e) {
988 return null;
989 }
990
991 final var contact = account.getContactStore().getContact(recipientId);
992 if (contact != null && !Util.isEmpty(contact.getName())) {
993 return contact.getName();
994 }
995
996 final var profile = context.getProfileHelper().getRecipientProfile(recipientId);
997 if (profile != null) {
998 return profile.getDisplayName();
999 }
1000
1001 return null;
1002 }
1003
1004 @Override
1005 public Group getGroup(GroupId groupId) {
1006 return toGroup(context.getGroupHelper().getGroup(groupId));
1007 }
1008
1009 @Override
1010 public List<Identity> getIdentities() {
1011 return account.getIdentityKeyStore().getIdentities().stream().map(this::toIdentity).toList();
1012 }
1013
1014 private Identity toIdentity(final IdentityInfo identityInfo) {
1015 if (identityInfo == null) {
1016 return null;
1017 }
1018
1019 final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId());
1020 final var scannableFingerprint = context.getIdentityHelper()
1021 .computeSafetyNumberForScanning(identityInfo.getRecipientId(), identityInfo.getIdentityKey());
1022 return new Identity(address,
1023 identityInfo.getIdentityKey(),
1024 context.getIdentityHelper()
1025 .computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()),
1026 scannableFingerprint == null ? null : scannableFingerprint.getSerialized(),
1027 identityInfo.getTrustLevel(),
1028 identityInfo.getDateAdded());
1029 }
1030
1031 @Override
1032 public List<Identity> getIdentities(RecipientIdentifier.Single recipient) {
1033 IdentityInfo identity;
1034 try {
1035 identity = account.getIdentityKeyStore()
1036 .getIdentity(context.getRecipientHelper().resolveRecipient(recipient));
1037 } catch (IOException | UnregisteredRecipientException e) {
1038 identity = null;
1039 }
1040 return identity == null ? List.of() : List.of(toIdentity(identity));
1041 }
1042
1043 /**
1044 * Trust this the identity with this fingerprint
1045 *
1046 * @param recipient account of the identity
1047 * @param fingerprint Fingerprint
1048 */
1049 @Override
1050 public boolean trustIdentityVerified(
1051 RecipientIdentifier.Single recipient, byte[] fingerprint
1052 ) throws UnregisteredRecipientException {
1053 RecipientId recipientId;
1054 try {
1055 recipientId = context.getRecipientHelper().resolveRecipient(recipient);
1056 } catch (IOException e) {
1057 return false;
1058 }
1059 final var updated = context.getIdentityHelper().trustIdentityVerified(recipientId, fingerprint);
1060 if (updated && this.isReceiving()) {
1061 context.getReceiveHelper().setNeedsToRetryFailedMessages(true);
1062 }
1063 return updated;
1064 }
1065
1066 /**
1067 * Trust this the identity with this safety number
1068 *
1069 * @param recipient account of the identity
1070 * @param safetyNumber Safety number
1071 */
1072 @Override
1073 public boolean trustIdentityVerifiedSafetyNumber(
1074 RecipientIdentifier.Single recipient, String safetyNumber
1075 ) throws UnregisteredRecipientException {
1076 RecipientId recipientId;
1077 try {
1078 recipientId = context.getRecipientHelper().resolveRecipient(recipient);
1079 } catch (IOException e) {
1080 return false;
1081 }
1082 final var updated = context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber);
1083 if (updated && this.isReceiving()) {
1084 context.getReceiveHelper().setNeedsToRetryFailedMessages(true);
1085 }
1086 return updated;
1087 }
1088
1089 /**
1090 * Trust this the identity with this scannable safety number
1091 *
1092 * @param recipient account of the identity
1093 * @param safetyNumber Scannable safety number
1094 */
1095 @Override
1096 public boolean trustIdentityVerifiedSafetyNumber(
1097 RecipientIdentifier.Single recipient, byte[] safetyNumber
1098 ) throws UnregisteredRecipientException {
1099 RecipientId recipientId;
1100 try {
1101 recipientId = context.getRecipientHelper().resolveRecipient(recipient);
1102 } catch (IOException e) {
1103 return false;
1104 }
1105 final var updated = context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber);
1106 if (updated && this.isReceiving()) {
1107 context.getReceiveHelper().setNeedsToRetryFailedMessages(true);
1108 }
1109 return updated;
1110 }
1111
1112 /**
1113 * Trust all keys of this identity without verification
1114 *
1115 * @param recipient account of the identity
1116 */
1117 @Override
1118 public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
1119 RecipientId recipientId;
1120 try {
1121 recipientId = context.getRecipientHelper().resolveRecipient(recipient);
1122 } catch (IOException e) {
1123 return false;
1124 }
1125 final var updated = context.getIdentityHelper().trustIdentityAllKeys(recipientId);
1126 if (updated && this.isReceiving()) {
1127 context.getReceiveHelper().setNeedsToRetryFailedMessages(true);
1128 }
1129 return updated;
1130 }
1131
1132 @Override
1133 public void addClosedListener(final Runnable listener) {
1134 synchronized (closedListeners) {
1135 closedListeners.add(listener);
1136 }
1137 }
1138
1139 @Override
1140 public void close() throws IOException {
1141 Thread thread;
1142 synchronized (messageHandlers) {
1143 weakHandlers.clear();
1144 messageHandlers.clear();
1145 thread = receiveThread;
1146 receiveThread = null;
1147 }
1148 if (thread != null) {
1149 stopReceiveThread(thread);
1150 }
1151 executor.shutdown();
1152
1153 dependencies.getSignalWebSocket().disconnect();
1154
1155 synchronized (closedListeners) {
1156 closedListeners.forEach(Runnable::run);
1157 closedListeners.clear();
1158 }
1159
1160 if (account != null) {
1161 account.close();
1162 }
1163 account = null;
1164 }
1165 }