]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/Manager.java
Update graalvm-config
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / Manager.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.Device;
20 import org.asamk.signal.manager.api.TypingAction;
21 import org.asamk.signal.manager.config.ServiceConfig;
22 import org.asamk.signal.manager.config.ServiceEnvironment;
23 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
24 import org.asamk.signal.manager.groups.GroupId;
25 import org.asamk.signal.manager.groups.GroupIdV1;
26 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
27 import org.asamk.signal.manager.groups.GroupLinkState;
28 import org.asamk.signal.manager.groups.GroupNotFoundException;
29 import org.asamk.signal.manager.groups.GroupPermission;
30 import org.asamk.signal.manager.groups.GroupUtils;
31 import org.asamk.signal.manager.groups.LastGroupAdminException;
32 import org.asamk.signal.manager.groups.NotAGroupMemberException;
33 import org.asamk.signal.manager.helper.GroupV2Helper;
34 import org.asamk.signal.manager.helper.PinHelper;
35 import org.asamk.signal.manager.helper.ProfileHelper;
36 import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
37 import org.asamk.signal.manager.jobs.Context;
38 import org.asamk.signal.manager.jobs.Job;
39 import org.asamk.signal.manager.jobs.RetrieveStickerPackJob;
40 import org.asamk.signal.manager.storage.SignalAccount;
41 import org.asamk.signal.manager.storage.groups.GroupInfo;
42 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
43 import org.asamk.signal.manager.storage.groups.GroupInfoV2;
44 import org.asamk.signal.manager.storage.identities.IdentityInfo;
45 import org.asamk.signal.manager.storage.messageCache.CachedMessage;
46 import org.asamk.signal.manager.storage.recipients.Contact;
47 import org.asamk.signal.manager.storage.recipients.Profile;
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.AttachmentUtils;
52 import org.asamk.signal.manager.util.IOUtils;
53 import org.asamk.signal.manager.util.KeyUtils;
54 import org.asamk.signal.manager.util.ProfileUtils;
55 import org.asamk.signal.manager.util.StickerUtils;
56 import org.asamk.signal.manager.util.Utils;
57 import org.signal.libsignal.metadata.InvalidMetadataMessageException;
58 import org.signal.libsignal.metadata.InvalidMetadataVersionException;
59 import org.signal.libsignal.metadata.ProtocolDuplicateMessageException;
60 import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
61 import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException;
62 import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
63 import org.signal.libsignal.metadata.ProtocolInvalidVersionException;
64 import org.signal.libsignal.metadata.ProtocolLegacyMessageException;
65 import org.signal.libsignal.metadata.ProtocolNoSessionException;
66 import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
67 import org.signal.libsignal.metadata.SelfSendException;
68 import org.signal.storageservice.protos.groups.GroupChange;
69 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
70 import org.signal.zkgroup.InvalidInputException;
71 import org.signal.zkgroup.VerificationFailedException;
72 import org.signal.zkgroup.groups.GroupMasterKey;
73 import org.signal.zkgroup.groups.GroupSecretParams;
74 import org.signal.zkgroup.profiles.ProfileKey;
75 import org.signal.zkgroup.profiles.ProfileKeyCredential;
76 import org.slf4j.Logger;
77 import org.slf4j.LoggerFactory;
78 import org.whispersystems.libsignal.IdentityKey;
79 import org.whispersystems.libsignal.IdentityKeyPair;
80 import org.whispersystems.libsignal.InvalidKeyException;
81 import org.whispersystems.libsignal.InvalidMessageException;
82 import org.whispersystems.libsignal.ecc.ECPublicKey;
83 import org.whispersystems.libsignal.state.PreKeyRecord;
84 import org.whispersystems.libsignal.state.SignedPreKeyRecord;
85 import org.whispersystems.libsignal.util.Pair;
86 import org.whispersystems.libsignal.util.guava.Optional;
87 import org.whispersystems.signalservice.api.InvalidMessageStructureException;
88 import org.whispersystems.signalservice.api.SignalSessionLock;
89 import org.whispersystems.signalservice.api.crypto.ContentHint;
90 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
91 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
92 import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
93 import org.whispersystems.signalservice.api.messages.SendMessageResult;
94 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
95 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
96 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
97 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
98 import org.whispersystems.signalservice.api.messages.SignalServiceContent;
99 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
100 import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
101 import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
102 import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
103 import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
104 import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
105 import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
106 import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
107 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
108 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream;
109 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream;
110 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup;
111 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream;
112 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream;
113 import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
114 import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
115 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
116 import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
117 import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
118 import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
119 import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
120 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
121 import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
122 import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
123 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
124 import org.whispersystems.signalservice.api.util.DeviceNameUtil;
125 import org.whispersystems.signalservice.api.util.InvalidNumberException;
126 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
127 import org.whispersystems.signalservice.api.util.UuidUtil;
128 import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
129 import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
130 import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
131 import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
132 import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
133 import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
134 import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
135 import org.whispersystems.signalservice.internal.util.Hex;
136 import org.whispersystems.signalservice.internal.util.Util;
137
138 import java.io.Closeable;
139 import java.io.File;
140 import java.io.FileInputStream;
141 import java.io.FileOutputStream;
142 import java.io.IOException;
143 import java.io.InputStream;
144 import java.io.OutputStream;
145 import java.net.URI;
146 import java.net.URISyntaxException;
147 import java.net.URLEncoder;
148 import java.nio.charset.StandardCharsets;
149 import java.nio.file.Files;
150 import java.security.SignatureException;
151 import java.util.ArrayList;
152 import java.util.Arrays;
153 import java.util.Base64;
154 import java.util.Collection;
155 import java.util.Date;
156 import java.util.HashSet;
157 import java.util.List;
158 import java.util.Map;
159 import java.util.Set;
160 import java.util.UUID;
161 import java.util.concurrent.ExecutorService;
162 import java.util.concurrent.Executors;
163 import java.util.concurrent.TimeUnit;
164 import java.util.concurrent.TimeoutException;
165 import java.util.concurrent.locks.ReentrantLock;
166 import java.util.function.Function;
167 import java.util.stream.Collectors;
168
169 import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
170
171 public class Manager implements Closeable {
172
173 private final static Logger logger = LoggerFactory.getLogger(Manager.class);
174
175 private final ServiceEnvironmentConfig serviceEnvironmentConfig;
176 private final SignalDependencies dependencies;
177
178 private SignalAccount account;
179
180 private final ExecutorService executor = Executors.newCachedThreadPool();
181
182 private final UnidentifiedAccessHelper unidentifiedAccessHelper;
183 private final ProfileHelper profileHelper;
184 private final GroupV2Helper groupV2Helper;
185 private final PinHelper pinHelper;
186 private final AvatarStore avatarStore;
187 private final AttachmentStore attachmentStore;
188 private final StickerPackStore stickerPackStore;
189 private final SignalSessionLock sessionLock = new SignalSessionLock() {
190 private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
191
192 @Override
193 public Lock acquire() {
194 LEGACY_LOCK.lock();
195 return LEGACY_LOCK::unlock;
196 }
197 };
198
199 Manager(
200 SignalAccount account,
201 PathConfig pathConfig,
202 ServiceEnvironmentConfig serviceEnvironmentConfig,
203 String userAgent
204 ) {
205 this.account = account;
206 this.serviceEnvironmentConfig = serviceEnvironmentConfig;
207
208 final var credentialsProvider = new DynamicCredentialsProvider(account.getUuid(),
209 account.getUsername(),
210 account.getPassword(),
211 account.getDeviceId());
212 this.dependencies = new SignalDependencies(account.getSelfAddress(),
213 serviceEnvironmentConfig,
214 userAgent,
215 credentialsProvider,
216 account.getSignalProtocolStore(),
217 executor,
218 sessionLock);
219 this.pinHelper = new PinHelper(dependencies.getKeyBackupService());
220
221 this.unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
222 account.getProfileStore()::getProfileKey,
223 this::getRecipientProfile,
224 this::getSenderCertificate);
225 this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey,
226 unidentifiedAccessHelper::getAccessFor,
227 dependencies::getProfileService,
228 dependencies::getMessageReceiver,
229 this::resolveSignalServiceAddress);
230 this.groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential,
231 this::getRecipientProfile,
232 account::getSelfRecipientId,
233 dependencies.getGroupsV2Operations(),
234 dependencies.getGroupsV2Api(),
235 this::getGroupAuthForToday,
236 this::resolveSignalServiceAddress);
237 this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
238 this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
239 this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath());
240 }
241
242 public String getUsername() {
243 return account.getUsername();
244 }
245
246 public SignalServiceAddress getSelfAddress() {
247 return account.getSelfAddress();
248 }
249
250 public RecipientId getSelfRecipientId() {
251 return account.getSelfRecipientId();
252 }
253
254 private IdentityKeyPair getIdentityKeyPair() {
255 return account.getIdentityKeyPair();
256 }
257
258 public int getDeviceId() {
259 return account.getDeviceId();
260 }
261
262 public static Manager init(
263 String username, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent
264 ) throws IOException, NotRegisteredException {
265 var pathConfig = PathConfig.createDefault(settingsPath);
266
267 if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) {
268 throw new NotRegisteredException();
269 }
270
271 var account = SignalAccount.load(pathConfig.getDataPath(), username, true);
272
273 if (!account.isRegistered()) {
274 throw new NotRegisteredException();
275 }
276
277 final var serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
278
279 return new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent);
280 }
281
282 public static List<String> getAllLocalUsernames(File settingsPath) {
283 var pathConfig = PathConfig.createDefault(settingsPath);
284 final var dataPath = pathConfig.getDataPath();
285 final var files = dataPath.listFiles();
286
287 if (files == null) {
288 return List.of();
289 }
290
291 return Arrays.stream(files)
292 .filter(File::isFile)
293 .map(File::getName)
294 .filter(file -> PhoneNumberFormatter.isValidNumber(file, null))
295 .collect(Collectors.toList());
296 }
297
298 public void checkAccountState() throws IOException {
299 if (account.getLastReceiveTimestamp() == 0) {
300 logger.warn("The Signal protocol expects that incoming messages are regularly received.");
301 } else {
302 var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp();
303 long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS);
304 if (days > 7) {
305 logger.warn(
306 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
307 days);
308 }
309 }
310 if (dependencies.getAccountManager().getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) {
311 refreshPreKeys();
312 }
313 if (account.getUuid() == null) {
314 account.setUuid(dependencies.getAccountManager().getOwnUuid());
315 }
316 updateAccountAttributes();
317 }
318
319 /**
320 * This is used for checking a set of phone numbers for registration on Signal
321 *
322 * @param numbers The set of phone number in question
323 * @return A map of numbers to booleans. True if registered, false otherwise. Should never be null
324 * @throws IOException if its unable to get the contacts to check if they're registered
325 */
326 public Map<String, Boolean> areUsersRegistered(Set<String> numbers) throws IOException {
327 // Note "contactDetails" has no optionals. It only gives us info on users who are registered
328 var contactDetails = getRegisteredUsers(numbers);
329
330 var registeredUsers = contactDetails.keySet();
331
332 return numbers.stream().collect(Collectors.toMap(x -> x, registeredUsers::contains));
333 }
334
335 public void updateAccountAttributes() throws IOException {
336 dependencies.getAccountManager()
337 .setAccountAttributes(account.getEncryptedDeviceName(),
338 null,
339 account.getLocalRegistrationId(),
340 true,
341 // set legacy pin only if no KBS master key is set
342 account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null,
343 account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
344 account.getSelfUnidentifiedAccessKey(),
345 account.isUnrestrictedUnidentifiedAccess(),
346 capabilities,
347 account.isDiscoverableByPhoneNumber());
348 }
349
350 /**
351 * @param givenName if null, the previous givenName will be kept
352 * @param familyName if null, the previous familyName will be kept
353 * @param about if null, the previous about text will be kept
354 * @param aboutEmoji if null, the previous about emoji will be kept
355 * @param avatar if avatar is null the image from the local avatar store is used (if present),
356 */
357 public void setProfile(
358 String givenName, final String familyName, String about, String aboutEmoji, Optional<File> avatar
359 ) throws IOException {
360 var profile = getRecipientProfile(account.getSelfRecipientId());
361 var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
362 if (givenName != null) {
363 builder.withGivenName(givenName);
364 }
365 if (familyName != null) {
366 builder.withFamilyName(familyName);
367 }
368 if (about != null) {
369 builder.withAbout(about);
370 }
371 if (aboutEmoji != null) {
372 builder.withAboutEmoji(aboutEmoji);
373 }
374 var newProfile = builder.build();
375
376 try (final var streamDetails = avatar == null
377 ? avatarStore.retrieveProfileAvatar(getSelfAddress())
378 : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
379 dependencies.getAccountManager()
380 .setVersionedProfile(account.getUuid(),
381 account.getProfileKey(),
382 newProfile.getInternalServiceName(),
383 newProfile.getAbout() == null ? "" : newProfile.getAbout(),
384 newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
385 Optional.absent(),
386 streamDetails);
387 }
388
389 if (avatar != null) {
390 if (avatar.isPresent()) {
391 avatarStore.storeProfileAvatar(getSelfAddress(),
392 outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream));
393 } else {
394 avatarStore.deleteProfileAvatar(getSelfAddress());
395 }
396 }
397 account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile);
398
399 try {
400 sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
401 } catch (UntrustedIdentityException ignored) {
402 }
403 }
404
405 public void unregister() throws IOException {
406 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
407 // If this is the master device, other users can't send messages to this number anymore.
408 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
409 dependencies.getAccountManager().setGcmId(Optional.absent());
410
411 account.setRegistered(false);
412 }
413
414 public void deleteAccount() throws IOException {
415 dependencies.getAccountManager().deleteAccount();
416
417 account.setRegistered(false);
418 }
419
420 public List<Device> getLinkedDevices() throws IOException {
421 var devices = dependencies.getAccountManager().getDevices();
422 account.setMultiDevice(devices.size() > 1);
423 var identityKey = account.getIdentityKeyPair().getPrivateKey();
424 return devices.stream().map(d -> {
425 String deviceName = d.getName();
426 if (deviceName != null) {
427 try {
428 deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey);
429 } catch (IOException e) {
430 logger.debug("Failed to decrypt device name, maybe plain text?", e);
431 }
432 }
433 return new Device(d.getId(), deviceName, d.getCreated(), d.getLastSeen());
434 }).collect(Collectors.toList());
435 }
436
437 public void removeLinkedDevices(int deviceId) throws IOException {
438 dependencies.getAccountManager().removeDevice(deviceId);
439 var devices = dependencies.getAccountManager().getDevices();
440 account.setMultiDevice(devices.size() > 1);
441 }
442
443 public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException {
444 var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri);
445
446 addDevice(info.deviceIdentifier, info.deviceKey);
447 }
448
449 private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException {
450 var identityKeyPair = getIdentityKeyPair();
451 var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
452
453 dependencies.getAccountManager()
454 .addDevice(deviceIdentifier,
455 deviceKey,
456 identityKeyPair,
457 Optional.of(account.getProfileKey().serialize()),
458 verificationCode);
459 account.setMultiDevice(true);
460 }
461
462 public void setRegistrationLockPin(Optional<String> pin) throws IOException, UnauthenticatedResponseException {
463 if (!account.isMasterDevice()) {
464 throw new RuntimeException("Only master device can set a PIN");
465 }
466 if (pin.isPresent()) {
467 final var masterKey = account.getPinMasterKey() != null
468 ? account.getPinMasterKey()
469 : KeyUtils.createMasterKey();
470
471 pinHelper.setRegistrationLockPin(pin.get(), masterKey);
472
473 account.setRegistrationLockPin(pin.get(), masterKey);
474 } else {
475 // Remove legacy registration lock
476 dependencies.getAccountManager().removeRegistrationLockV1();
477
478 // Remove KBS Pin
479 pinHelper.removeRegistrationLockPin();
480
481 account.setRegistrationLockPin(null, null);
482 }
483 }
484
485 void refreshPreKeys() throws IOException {
486 var oneTimePreKeys = generatePreKeys();
487 final var identityKeyPair = getIdentityKeyPair();
488 var signedPreKeyRecord = generateSignedPreKey(identityKeyPair);
489
490 dependencies.getAccountManager().setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys);
491 }
492
493 private List<PreKeyRecord> generatePreKeys() {
494 final var offset = account.getPreKeyIdOffset();
495
496 var records = KeyUtils.generatePreKeyRecords(offset, ServiceConfig.PREKEY_BATCH_SIZE);
497 account.addPreKeys(records);
498
499 return records;
500 }
501
502 private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
503 final var signedPreKeyId = account.getNextSignedPreKeyId();
504
505 var record = KeyUtils.generateSignedPreKeyRecord(identityKeyPair, signedPreKeyId);
506 account.addSignedPreKey(record);
507
508 return record;
509 }
510
511 public Profile getRecipientProfile(
512 RecipientId recipientId
513 ) {
514 return getRecipientProfile(recipientId, false);
515 }
516
517 private final Set<RecipientId> pendingProfileRequest = new HashSet<>();
518
519 Profile getRecipientProfile(
520 RecipientId recipientId, boolean force
521 ) {
522 var profile = account.getProfileStore().getProfile(recipientId);
523
524 var now = System.currentTimeMillis();
525 // Profiles are cached for 24h before retrieving them again, unless forced
526 if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) {
527 return profile;
528 }
529
530 synchronized (pendingProfileRequest) {
531 if (pendingProfileRequest.contains(recipientId)) {
532 return profile;
533 }
534 pendingProfileRequest.add(recipientId);
535 }
536 final SignalServiceProfile encryptedProfile;
537 try {
538 encryptedProfile = retrieveEncryptedProfile(recipientId);
539 } finally {
540 synchronized (pendingProfileRequest) {
541 pendingProfileRequest.remove(recipientId);
542 }
543 }
544 if (encryptedProfile == null) {
545 return null;
546 }
547
548 profile = decryptProfileIfKeyKnown(recipientId, encryptedProfile);
549 account.getProfileStore().storeProfile(recipientId, profile);
550
551 return profile;
552 }
553
554 private Profile decryptProfileIfKeyKnown(
555 final RecipientId recipientId, final SignalServiceProfile encryptedProfile
556 ) {
557 var profileKey = account.getProfileStore().getProfileKey(recipientId);
558 if (profileKey == null) {
559 return new Profile(System.currentTimeMillis(),
560 null,
561 null,
562 null,
563 null,
564 ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null),
565 ProfileUtils.getCapabilities(encryptedProfile));
566 }
567
568 return decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile);
569 }
570
571 private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) {
572 try {
573 return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile();
574 } catch (IOException e) {
575 logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
576 return null;
577 }
578 }
579
580 private ProfileAndCredential retrieveProfileAndCredential(
581 final RecipientId recipientId, final SignalServiceProfile.RequestType requestType
582 ) throws IOException {
583 final var profileAndCredential = profileHelper.retrieveProfileSync(recipientId, requestType);
584 final var profile = profileAndCredential.getProfile();
585
586 try {
587 var newIdentity = account.getIdentityKeyStore()
588 .saveIdentity(recipientId,
589 new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())),
590 new Date());
591
592 if (newIdentity) {
593 account.getSessionStore().archiveSessions(recipientId);
594 }
595 } catch (InvalidKeyException ignored) {
596 logger.warn("Got invalid identity key in profile for {}",
597 resolveSignalServiceAddress(recipientId).getIdentifier());
598 }
599 return profileAndCredential;
600 }
601
602 private ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) {
603 var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId);
604 if (profileKeyCredential != null) {
605 return profileKeyCredential;
606 }
607
608 ProfileAndCredential profileAndCredential;
609 try {
610 profileAndCredential = retrieveProfileAndCredential(recipientId,
611 SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL);
612 } catch (IOException e) {
613 logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
614 return null;
615 }
616
617 profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull();
618 account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential);
619
620 var profileKey = account.getProfileStore().getProfileKey(recipientId);
621 if (profileKey != null) {
622 final var profile = decryptProfileAndDownloadAvatar(recipientId,
623 profileKey,
624 profileAndCredential.getProfile());
625 account.getProfileStore().storeProfile(recipientId, profile);
626 }
627
628 return profileKeyCredential;
629 }
630
631 private Profile decryptProfileAndDownloadAvatar(
632 final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
633 ) {
634 if (encryptedProfile.getAvatar() != null) {
635 downloadProfileAvatar(resolveSignalServiceAddress(recipientId), encryptedProfile.getAvatar(), profileKey);
636 }
637
638 return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
639 }
640
641 private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(GroupId groupId) throws IOException {
642 final var streamDetails = avatarStore.retrieveGroupAvatar(groupId);
643 if (streamDetails == null) {
644 return Optional.absent();
645 }
646
647 return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent()));
648 }
649
650 private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(SignalServiceAddress address) throws IOException {
651 final var streamDetails = avatarStore.retrieveContactAvatar(address);
652 if (streamDetails == null) {
653 return Optional.absent();
654 }
655
656 return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent()));
657 }
658
659 private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
660 var g = getGroup(groupId);
661 if (g == null) {
662 throw new GroupNotFoundException(groupId);
663 }
664 if (!g.isMember(account.getSelfRecipientId())) {
665 throw new NotAGroupMemberException(groupId, g.getTitle());
666 }
667 return g;
668 }
669
670 private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
671 var g = getGroup(groupId);
672 if (g == null) {
673 throw new GroupNotFoundException(groupId);
674 }
675 if (!g.isMember(account.getSelfRecipientId()) && !g.isPendingMember(account.getSelfRecipientId())) {
676 throw new NotAGroupMemberException(groupId, g.getTitle());
677 }
678 return g;
679 }
680
681 public List<GroupInfo> getGroups() {
682 return account.getGroupStore().getGroups();
683 }
684
685 public Pair<Long, List<SendMessageResult>> sendGroupMessage(
686 String messageText, List<String> attachments, GroupId groupId
687 ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
688 final var messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
689 if (attachments != null) {
690 messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments));
691 }
692
693 return sendGroupMessage(messageBuilder, groupId);
694 }
695
696 public Pair<Long, List<SendMessageResult>> sendGroupMessageReaction(
697 String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, GroupId groupId
698 ) throws IOException, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException {
699 var targetAuthorRecipientId = canonicalizeAndResolveRecipient(targetAuthor);
700 var reaction = new SignalServiceDataMessage.Reaction(emoji,
701 remove,
702 resolveSignalServiceAddress(targetAuthorRecipientId),
703 targetSentTimestamp);
704 final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction);
705
706 return sendGroupMessage(messageBuilder, groupId);
707 }
708
709 public Pair<Long, List<SendMessageResult>> sendGroupMessage(
710 SignalServiceDataMessage.Builder messageBuilder, GroupId groupId
711 ) throws IOException, GroupNotFoundException, NotAGroupMemberException {
712 final var g = getGroupForSending(groupId);
713
714 GroupUtils.setGroupContext(messageBuilder, g);
715 messageBuilder.withExpiration(g.getMessageExpirationTime());
716
717 return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
718 }
719
720 public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(
721 GroupId groupId, Set<String> groupAdmins
722 ) throws GroupNotFoundException, IOException, NotAGroupMemberException, InvalidNumberException, LastGroupAdminException {
723 var group = getGroupForUpdating(groupId);
724 if (group instanceof GroupInfoV1) {
725 return quitGroupV1((GroupInfoV1) group);
726 }
727
728 final var newAdmins = getSignalServiceAddresses(groupAdmins);
729 try {
730 return quitGroupV2((GroupInfoV2) group, newAdmins);
731 } catch (ConflictException e) {
732 // Detected conflicting update, refreshing group and trying again
733 group = getGroup(groupId, true);
734 return quitGroupV2((GroupInfoV2) group, newAdmins);
735 }
736 }
737
738 private Pair<Long, List<SendMessageResult>> quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException {
739 var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
740 .withId(groupInfoV1.getGroupId().serialize())
741 .build();
742
743 var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
744 groupInfoV1.removeMember(account.getSelfRecipientId());
745 account.getGroupStore().updateGroup(groupInfoV1);
746 return sendMessage(messageBuilder, groupInfoV1.getMembersWithout(account.getSelfRecipientId()));
747 }
748
749 private Pair<Long, List<SendMessageResult>> quitGroupV2(
750 final GroupInfoV2 groupInfoV2, final Set<RecipientId> newAdmins
751 ) throws LastGroupAdminException, IOException {
752 final var currentAdmins = groupInfoV2.getAdminMembers();
753 newAdmins.removeAll(currentAdmins);
754 newAdmins.retainAll(groupInfoV2.getMembers());
755 if (currentAdmins.contains(getSelfRecipientId())
756 && currentAdmins.size() == 1
757 && groupInfoV2.getMembers().size() > 1
758 && newAdmins.size() == 0) {
759 // Last admin can't leave the group, unless she's also the last member
760 throw new LastGroupAdminException(groupInfoV2.getGroupId(), groupInfoV2.getTitle());
761 }
762 final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins);
763 groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient);
764 var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
765 account.getGroupStore().updateGroup(groupInfoV2);
766 return sendMessage(messageBuilder, groupInfoV2.getMembersWithout(account.getSelfRecipientId()));
767 }
768
769 public void deleteGroup(GroupId groupId) throws IOException {
770 account.getGroupStore().deleteGroup(groupId);
771 avatarStore.deleteGroupAvatar(groupId);
772 }
773
774 public Pair<GroupId, List<SendMessageResult>> createGroup(
775 String name, List<String> members, File avatarFile
776 ) throws IOException, AttachmentInvalidException, InvalidNumberException {
777 return createGroup(name, members == null ? null : getSignalServiceAddresses(members), avatarFile);
778 }
779
780 private Pair<GroupId, List<SendMessageResult>> createGroup(
781 String name, Set<RecipientId> members, File avatarFile
782 ) throws IOException, AttachmentInvalidException {
783 final var selfRecipientId = account.getSelfRecipientId();
784 if (members != null && members.contains(selfRecipientId)) {
785 members = new HashSet<>(members);
786 members.remove(selfRecipientId);
787 }
788
789 var gv2Pair = groupV2Helper.createGroup(name == null ? "" : name,
790 members == null ? Set.of() : members,
791 avatarFile);
792
793 SignalServiceDataMessage.Builder messageBuilder;
794 if (gv2Pair == null) {
795 // Failed to create v2 group, creating v1 group instead
796 var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
797 gv1.addMembers(List.of(selfRecipientId));
798 final var result = updateGroupV1(gv1, name, members, avatarFile);
799 return new Pair<>(gv1.getGroupId(), result.second());
800 }
801
802 final var gv2 = gv2Pair.first();
803 final var decryptedGroup = gv2Pair.second();
804
805 gv2.setGroup(decryptedGroup, this::resolveRecipient);
806 if (avatarFile != null) {
807 avatarStore.storeGroupAvatar(gv2.getGroupId(),
808 outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
809 }
810 messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
811 account.getGroupStore().updateGroup(gv2);
812
813 final var result = sendMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId));
814 return new Pair<>(gv2.getGroupId(), result.second());
815 }
816
817 public Pair<Long, List<SendMessageResult>> updateGroup(
818 GroupId groupId,
819 String name,
820 String description,
821 List<String> members,
822 List<String> removeMembers,
823 List<String> admins,
824 List<String> removeAdmins,
825 boolean resetGroupLink,
826 GroupLinkState groupLinkState,
827 GroupPermission addMemberPermission,
828 GroupPermission editDetailsPermission,
829 File avatarFile,
830 Integer expirationTimer
831 ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
832 return updateGroup(groupId,
833 name,
834 description,
835 members == null ? null : getSignalServiceAddresses(members),
836 removeMembers == null ? null : getSignalServiceAddresses(removeMembers),
837 admins == null ? null : getSignalServiceAddresses(admins),
838 removeAdmins == null ? null : getSignalServiceAddresses(removeAdmins),
839 resetGroupLink,
840 groupLinkState,
841 addMemberPermission,
842 editDetailsPermission,
843 avatarFile,
844 expirationTimer);
845 }
846
847 private Pair<Long, List<SendMessageResult>> updateGroup(
848 final GroupId groupId,
849 final String name,
850 final String description,
851 final Set<RecipientId> members,
852 final Set<RecipientId> removeMembers,
853 final Set<RecipientId> admins,
854 final Set<RecipientId> removeAdmins,
855 final boolean resetGroupLink,
856 final GroupLinkState groupLinkState,
857 final GroupPermission addMemberPermission,
858 final GroupPermission editDetailsPermission,
859 final File avatarFile,
860 final Integer expirationTimer
861 ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
862 var group = getGroupForUpdating(groupId);
863
864 if (group instanceof GroupInfoV2) {
865 try {
866 return updateGroupV2((GroupInfoV2) group,
867 name,
868 description,
869 members,
870 removeMembers,
871 admins,
872 removeAdmins,
873 resetGroupLink,
874 groupLinkState,
875 addMemberPermission,
876 editDetailsPermission,
877 avatarFile,
878 expirationTimer);
879 } catch (ConflictException e) {
880 // Detected conflicting update, refreshing group and trying again
881 group = getGroup(groupId, true);
882 return updateGroupV2((GroupInfoV2) group,
883 name,
884 description,
885 members,
886 removeMembers,
887 admins,
888 removeAdmins,
889 resetGroupLink,
890 groupLinkState,
891 addMemberPermission,
892 editDetailsPermission,
893 avatarFile,
894 expirationTimer);
895 }
896 }
897
898 final var gv1 = (GroupInfoV1) group;
899 final var result = updateGroupV1(gv1, name, members, avatarFile);
900 if (expirationTimer != null) {
901 setExpirationTimer(gv1, expirationTimer);
902 }
903 return result;
904 }
905
906 private Pair<Long, List<SendMessageResult>> updateGroupV1(
907 final GroupInfoV1 gv1, final String name, final Set<RecipientId> members, final File avatarFile
908 ) throws IOException, AttachmentInvalidException {
909 updateGroupV1Details(gv1, name, members, avatarFile);
910 var messageBuilder = getGroupUpdateMessageBuilder(gv1);
911
912 account.getGroupStore().updateGroup(gv1);
913
914 return sendMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
915 }
916
917 private void updateGroupV1Details(
918 final GroupInfoV1 g, final String name, final Collection<RecipientId> members, final File avatarFile
919 ) throws IOException {
920 if (name != null) {
921 g.name = name;
922 }
923
924 if (members != null) {
925 final var newMemberAddresses = members.stream()
926 .filter(member -> !g.isMember(member))
927 .map(this::resolveSignalServiceAddress)
928 .collect(Collectors.toList());
929 final var newE164Members = new HashSet<String>();
930 for (var member : newMemberAddresses) {
931 if (!member.getNumber().isPresent()) {
932 continue;
933 }
934 newE164Members.add(member.getNumber().get());
935 }
936
937 final var registeredUsers = getRegisteredUsers(newE164Members);
938 if (registeredUsers.size() != newE164Members.size()) {
939 // Some of the new members are not registered on Signal
940 newE164Members.removeAll(registeredUsers.keySet());
941 throw new IOException("Failed to add members "
942 + String.join(", ", newE164Members)
943 + " to group: Not registered on Signal");
944 }
945
946 g.addMembers(members);
947 }
948
949 if (avatarFile != null) {
950 avatarStore.storeGroupAvatar(g.getGroupId(),
951 outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
952 }
953 }
954
955 private Pair<Long, List<SendMessageResult>> updateGroupV2(
956 final GroupInfoV2 group,
957 final String name,
958 final String description,
959 final Set<RecipientId> members,
960 final Set<RecipientId> removeMembers,
961 final Set<RecipientId> admins,
962 final Set<RecipientId> removeAdmins,
963 final boolean resetGroupLink,
964 final GroupLinkState groupLinkState,
965 final GroupPermission addMemberPermission,
966 final GroupPermission editDetailsPermission,
967 final File avatarFile,
968 Integer expirationTimer
969 ) throws IOException {
970 Pair<Long, List<SendMessageResult>> result = null;
971 if (group.isPendingMember(account.getSelfRecipientId())) {
972 var groupGroupChangePair = groupV2Helper.acceptInvite(group);
973 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
974 }
975
976 if (members != null) {
977 final var newMembers = new HashSet<>(members);
978 newMembers.removeAll(group.getMembers());
979 if (newMembers.size() > 0) {
980 var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers);
981 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
982 }
983 }
984
985 if (removeMembers != null) {
986 var existingRemoveMembers = new HashSet<>(removeMembers);
987 existingRemoveMembers.retainAll(group.getMembers());
988 existingRemoveMembers.remove(getSelfRecipientId());// self can be removed with sendQuitGroupMessage
989 if (existingRemoveMembers.size() > 0) {
990 var groupGroupChangePair = groupV2Helper.removeMembers(group, existingRemoveMembers);
991 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
992 }
993
994 var pendingRemoveMembers = new HashSet<>(removeMembers);
995 pendingRemoveMembers.retainAll(group.getPendingMembers());
996 if (pendingRemoveMembers.size() > 0) {
997 var groupGroupChangePair = groupV2Helper.revokeInvitedMembers(group, pendingRemoveMembers);
998 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
999 }
1000 }
1001
1002 if (admins != null) {
1003 final var newAdmins = new HashSet<>(admins);
1004 newAdmins.retainAll(group.getMembers());
1005 newAdmins.removeAll(group.getAdminMembers());
1006 if (newAdmins.size() > 0) {
1007 for (var admin : newAdmins) {
1008 var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true);
1009 result = sendUpdateGroupV2Message(group,
1010 groupGroupChangePair.first(),
1011 groupGroupChangePair.second());
1012 }
1013 }
1014 }
1015
1016 if (removeAdmins != null) {
1017 final var existingRemoveAdmins = new HashSet<>(removeAdmins);
1018 existingRemoveAdmins.retainAll(group.getAdminMembers());
1019 if (existingRemoveAdmins.size() > 0) {
1020 for (var admin : existingRemoveAdmins) {
1021 var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false);
1022 result = sendUpdateGroupV2Message(group,
1023 groupGroupChangePair.first(),
1024 groupGroupChangePair.second());
1025 }
1026 }
1027 }
1028
1029 if (resetGroupLink) {
1030 var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group);
1031 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
1032 }
1033
1034 if (groupLinkState != null) {
1035 var groupGroupChangePair = groupV2Helper.setGroupLinkState(group, groupLinkState);
1036 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
1037 }
1038
1039 if (addMemberPermission != null) {
1040 var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission);
1041 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
1042 }
1043
1044 if (editDetailsPermission != null) {
1045 var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission);
1046 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
1047 }
1048
1049 if (expirationTimer != null) {
1050 var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer);
1051 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
1052 }
1053
1054 if (name != null || description != null || avatarFile != null) {
1055 var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile);
1056 if (avatarFile != null) {
1057 avatarStore.storeGroupAvatar(group.getGroupId(),
1058 outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
1059 }
1060 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
1061 }
1062
1063 return result;
1064 }
1065
1066 public Pair<GroupId, List<SendMessageResult>> joinGroup(
1067 GroupInviteLinkUrl inviteLinkUrl
1068 ) throws IOException, GroupLinkNotActiveException {
1069 final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(),
1070 inviteLinkUrl.getPassword());
1071 final var groupChange = groupV2Helper.joinGroup(inviteLinkUrl.getGroupMasterKey(),
1072 inviteLinkUrl.getPassword(),
1073 groupJoinInfo);
1074 final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(),
1075 groupJoinInfo.getRevision() + 1,
1076 groupChange.toByteArray());
1077
1078 if (group.getGroup() == null) {
1079 // Only requested member, can't send update to group members
1080 return new Pair<>(group.getGroupId(), List.of());
1081 }
1082
1083 final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange);
1084
1085 return new Pair<>(group.getGroupId(), result.second());
1086 }
1087
1088 private Pair<Long, List<SendMessageResult>> sendUpdateGroupV2Message(
1089 GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
1090 ) throws IOException {
1091 final var selfRecipientId = account.getSelfRecipientId();
1092 final var members = group.getMembersIncludingPendingWithout(selfRecipientId);
1093 group.setGroup(newDecryptedGroup, this::resolveRecipient);
1094 members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId));
1095
1096 final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
1097 account.getGroupStore().updateGroup(group);
1098 return sendMessage(messageBuilder, members);
1099 }
1100
1101 private static int currentTimeDays() {
1102 return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
1103 }
1104
1105 private GroupsV2AuthorizationString getGroupAuthForToday(
1106 final GroupSecretParams groupSecretParams
1107 ) throws IOException {
1108 final var today = currentTimeDays();
1109 // Returns credentials for the next 7 days
1110 final var credentials = dependencies.getGroupsV2Api().getCredentials(today);
1111 // TODO cache credentials until they expire
1112 var authCredentialResponse = credentials.get(today);
1113 try {
1114 return dependencies.getGroupsV2Api()
1115 .getGroupsV2AuthorizationString(account.getUuid(),
1116 today,
1117 groupSecretParams,
1118 authCredentialResponse);
1119 } catch (VerificationFailedException e) {
1120 throw new IOException(e);
1121 }
1122 }
1123
1124 Pair<Long, List<SendMessageResult>> sendGroupInfoMessage(
1125 GroupIdV1 groupId, SignalServiceAddress recipient
1126 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
1127 GroupInfoV1 g;
1128 var group = getGroupForSending(groupId);
1129 if (!(group instanceof GroupInfoV1)) {
1130 throw new RuntimeException("Received an invalid group request for a v2 group!");
1131 }
1132 g = (GroupInfoV1) group;
1133
1134 final var recipientId = resolveRecipient(recipient);
1135 if (!g.isMember(recipientId)) {
1136 throw new NotAGroupMemberException(groupId, g.name);
1137 }
1138
1139 var messageBuilder = getGroupUpdateMessageBuilder(g);
1140
1141 // Send group message only to the recipient who requested it
1142 return sendMessage(messageBuilder, Set.of(recipientId));
1143 }
1144
1145 private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException {
1146 var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
1147 .withId(g.getGroupId().serialize())
1148 .withName(g.name)
1149 .withMembers(g.getMembers()
1150 .stream()
1151 .map(this::resolveSignalServiceAddress)
1152 .collect(Collectors.toList()));
1153
1154 try {
1155 final var attachment = createGroupAvatarAttachment(g.getGroupId());
1156 if (attachment.isPresent()) {
1157 group.withAvatar(attachment.get());
1158 }
1159 } catch (IOException e) {
1160 throw new AttachmentInvalidException(g.getGroupId().toBase64(), e);
1161 }
1162
1163 return SignalServiceDataMessage.newBuilder()
1164 .asGroupMessage(group.build())
1165 .withExpiration(g.getMessageExpirationTime());
1166 }
1167
1168 private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
1169 var group = SignalServiceGroupV2.newBuilder(g.getMasterKey())
1170 .withRevision(g.getGroup().getRevision())
1171 .withSignedGroupChange(signedGroupChange);
1172 return SignalServiceDataMessage.newBuilder()
1173 .asGroupMessage(group.build())
1174 .withExpiration(g.getMessageExpirationTime());
1175 }
1176
1177 Pair<Long, List<SendMessageResult>> sendGroupInfoRequest(
1178 GroupIdV1 groupId, SignalServiceAddress recipient
1179 ) throws IOException {
1180 var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize());
1181
1182 var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
1183
1184 // Send group info request message to the recipient who sent us a message with this groupId
1185 return sendMessage(messageBuilder, Set.of(resolveRecipient(recipient)));
1186 }
1187
1188 void sendReceipt(
1189 SignalServiceAddress remoteAddress, long messageId
1190 ) throws IOException, UntrustedIdentityException {
1191 var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY,
1192 List.of(messageId),
1193 System.currentTimeMillis());
1194
1195 dependencies.getMessageSender()
1196 .sendReceipt(remoteAddress,
1197 unidentifiedAccessHelper.getAccessFor(resolveRecipient(remoteAddress)),
1198 receiptMessage);
1199 }
1200
1201 public Pair<Long, List<SendMessageResult>> sendMessage(
1202 String messageText, List<String> attachments, List<String> recipients
1203 ) throws IOException, AttachmentInvalidException, InvalidNumberException {
1204 final var messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
1205 if (attachments != null) {
1206 var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments);
1207
1208 // Upload attachments here, so we only upload once even for multiple recipients
1209 var messageSender = dependencies.getMessageSender();
1210 var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
1211 for (var attachment : attachmentStreams) {
1212 if (attachment.isStream()) {
1213 attachmentPointers.add(messageSender.uploadAttachment(attachment.asStream()));
1214 } else if (attachment.isPointer()) {
1215 attachmentPointers.add(attachment.asPointer());
1216 }
1217 }
1218
1219 messageBuilder.withAttachments(attachmentPointers);
1220 }
1221 return sendMessage(messageBuilder, getSignalServiceAddresses(recipients));
1222 }
1223
1224 public Pair<Long, SendMessageResult> sendSelfMessage(
1225 String messageText, List<String> attachments
1226 ) throws IOException, AttachmentInvalidException {
1227 final var messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
1228 if (attachments != null) {
1229 messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments));
1230 }
1231 return sendSelfMessage(messageBuilder);
1232 }
1233
1234 public Pair<Long, List<SendMessageResult>> sendRemoteDeleteMessage(
1235 long targetSentTimestamp, List<String> recipients
1236 ) throws IOException, InvalidNumberException {
1237 var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
1238 final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
1239 return sendMessage(messageBuilder, getSignalServiceAddresses(recipients));
1240 }
1241
1242 public Pair<Long, List<SendMessageResult>> sendGroupRemoteDeleteMessage(
1243 long targetSentTimestamp, GroupId groupId
1244 ) throws IOException, NotAGroupMemberException, GroupNotFoundException {
1245 var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
1246 final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
1247 return sendGroupMessage(messageBuilder, groupId);
1248 }
1249
1250 public Pair<Long, List<SendMessageResult>> sendMessageReaction(
1251 String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List<String> recipients
1252 ) throws IOException, InvalidNumberException {
1253 var targetAuthorRecipientId = canonicalizeAndResolveRecipient(targetAuthor);
1254 var reaction = new SignalServiceDataMessage.Reaction(emoji,
1255 remove,
1256 resolveSignalServiceAddress(targetAuthorRecipientId),
1257 targetSentTimestamp);
1258 final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction);
1259 return sendMessage(messageBuilder, getSignalServiceAddresses(recipients));
1260 }
1261
1262 public Pair<Long, List<SendMessageResult>> sendEndSessionMessage(List<String> recipients) throws IOException, InvalidNumberException {
1263 var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
1264
1265 final var signalServiceAddresses = getSignalServiceAddresses(recipients);
1266 try {
1267 return sendMessage(messageBuilder, signalServiceAddresses);
1268 } catch (Exception e) {
1269 for (var address : signalServiceAddresses) {
1270 handleEndSession(address);
1271 }
1272 throw e;
1273 }
1274 }
1275
1276 void renewSession(RecipientId recipientId) throws IOException {
1277 account.getSessionStore().archiveSessions(recipientId);
1278 if (!recipientId.equals(getSelfRecipientId())) {
1279 sendNullMessage(recipientId);
1280 }
1281 }
1282
1283 public void setContactName(String number, String name) throws InvalidNumberException, NotMasterDeviceException {
1284 if (!account.isMasterDevice()) {
1285 throw new NotMasterDeviceException();
1286 }
1287 final var recipientId = canonicalizeAndResolveRecipient(number);
1288 var contact = account.getContactStore().getContact(recipientId);
1289 final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
1290 account.getContactStore().storeContact(recipientId, builder.withName(name).build());
1291 }
1292
1293 public void setContactBlocked(
1294 String number, boolean blocked
1295 ) throws InvalidNumberException, NotMasterDeviceException {
1296 if (!account.isMasterDevice()) {
1297 throw new NotMasterDeviceException();
1298 }
1299 setContactBlocked(canonicalizeAndResolveRecipient(number), blocked);
1300 }
1301
1302 private void setContactBlocked(RecipientId recipientId, boolean blocked) {
1303 var contact = account.getContactStore().getContact(recipientId);
1304 final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
1305 account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build());
1306 }
1307
1308 public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException {
1309 var group = getGroup(groupId);
1310 if (group == null) {
1311 throw new GroupNotFoundException(groupId);
1312 }
1313
1314 group.setBlocked(blocked);
1315 account.getGroupStore().updateGroup(group);
1316 }
1317
1318 private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) {
1319 var contact = account.getContactStore().getContact(recipientId);
1320 if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) {
1321 return;
1322 }
1323 final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
1324 account.getContactStore()
1325 .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build());
1326 }
1327
1328 private void sendExpirationTimerUpdate(RecipientId recipientId) throws IOException {
1329 final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
1330 sendMessage(messageBuilder, Set.of(recipientId));
1331 }
1332
1333 /**
1334 * Change the expiration timer for a contact
1335 */
1336 public void setExpirationTimer(
1337 String number, int messageExpirationTimer
1338 ) throws IOException, InvalidNumberException {
1339 var recipientId = canonicalizeAndResolveRecipient(number);
1340 setExpirationTimer(recipientId, messageExpirationTimer);
1341 sendExpirationTimerUpdate(recipientId);
1342 }
1343
1344 /**
1345 * Change the expiration timer for a group
1346 */
1347 private void setExpirationTimer(
1348 GroupInfoV1 groupInfoV1, int messageExpirationTimer
1349 ) throws NotAGroupMemberException, GroupNotFoundException, IOException {
1350 groupInfoV1.messageExpirationTime = messageExpirationTimer;
1351 account.getGroupStore().updateGroup(groupInfoV1);
1352 sendExpirationTimerUpdate(groupInfoV1.getGroupId());
1353 }
1354
1355 private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException {
1356 final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
1357 sendGroupMessage(messageBuilder, groupId);
1358 }
1359
1360 /**
1361 * Upload the sticker pack from path.
1362 *
1363 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
1364 * @return if successful, returns the URL to install the sticker pack in the signal app
1365 */
1366 public String uploadStickerPack(File path) throws IOException, StickerPackInvalidException {
1367 var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path);
1368
1369 var messageSender = dependencies.getMessageSender();
1370
1371 var packKey = KeyUtils.createStickerUploadKey();
1372 var packIdString = messageSender.uploadStickerManifest(manifest, packKey);
1373 var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString));
1374
1375 var sticker = new Sticker(packId, packKey);
1376 account.getStickerStore().updateSticker(sticker);
1377
1378 try {
1379 return new URI("https",
1380 "signal.art",
1381 "/addstickers/",
1382 "pack_id="
1383 + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8)
1384 + "&pack_key="
1385 + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)).toString();
1386 } catch (URISyntaxException e) {
1387 throw new AssertionError(e);
1388 }
1389 }
1390
1391 public void requestAllSyncData() throws IOException {
1392 requestSyncGroups();
1393 requestSyncContacts();
1394 requestSyncBlocked();
1395 requestSyncConfiguration();
1396 requestSyncKeys();
1397 }
1398
1399 private void requestSyncGroups() throws IOException {
1400 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
1401 .setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS)
1402 .build();
1403 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
1404 try {
1405 sendSyncMessage(message);
1406 } catch (UntrustedIdentityException e) {
1407 throw new AssertionError(e);
1408 }
1409 }
1410
1411 private void requestSyncContacts() throws IOException {
1412 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
1413 .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS)
1414 .build();
1415 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
1416 try {
1417 sendSyncMessage(message);
1418 } catch (UntrustedIdentityException e) {
1419 throw new AssertionError(e);
1420 }
1421 }
1422
1423 private void requestSyncBlocked() throws IOException {
1424 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
1425 .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED)
1426 .build();
1427 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
1428 try {
1429 sendSyncMessage(message);
1430 } catch (UntrustedIdentityException e) {
1431 throw new AssertionError(e);
1432 }
1433 }
1434
1435 private void requestSyncConfiguration() throws IOException {
1436 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
1437 .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION)
1438 .build();
1439 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
1440 try {
1441 sendSyncMessage(message);
1442 } catch (UntrustedIdentityException e) {
1443 throw new AssertionError(e);
1444 }
1445 }
1446
1447 private void requestSyncKeys() throws IOException {
1448 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
1449 .setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS)
1450 .build();
1451 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
1452 try {
1453 sendSyncMessage(message);
1454 } catch (UntrustedIdentityException e) {
1455 throw new AssertionError(e);
1456 }
1457 }
1458
1459 private byte[] getSenderCertificate() {
1460 byte[] certificate;
1461 try {
1462 if (account.isPhoneNumberShared()) {
1463 certificate = dependencies.getAccountManager().getSenderCertificate();
1464 } else {
1465 certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy();
1466 }
1467 } catch (IOException e) {
1468 logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
1469 return null;
1470 }
1471 // TODO cache for a day
1472 return certificate;
1473 }
1474
1475 private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException {
1476 var messageSender = dependencies.getMessageSender();
1477 messageSender.sendSyncMessage(message, unidentifiedAccessHelper.getAccessForSync());
1478 }
1479
1480 private Set<RecipientId> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
1481 final var signalServiceAddresses = new HashSet<SignalServiceAddress>(numbers.size());
1482 final var addressesMissingUuid = new HashSet<SignalServiceAddress>();
1483
1484 for (var number : numbers) {
1485 final var resolvedAddress = resolveSignalServiceAddress(canonicalizeAndResolveRecipient(number));
1486 if (resolvedAddress.getUuid().isPresent()) {
1487 signalServiceAddresses.add(resolvedAddress);
1488 } else {
1489 addressesMissingUuid.add(resolvedAddress);
1490 }
1491 }
1492
1493 final var numbersMissingUuid = addressesMissingUuid.stream()
1494 .map(a -> a.getNumber().get())
1495 .collect(Collectors.toSet());
1496 Map<String, UUID> registeredUsers;
1497 try {
1498 registeredUsers = getRegisteredUsers(numbersMissingUuid);
1499 } catch (IOException e) {
1500 logger.warn("Failed to resolve uuids from server, ignoring: {}", e.getMessage());
1501 registeredUsers = Map.of();
1502 }
1503
1504 for (var address : addressesMissingUuid) {
1505 final var number = address.getNumber().get();
1506 if (registeredUsers.containsKey(number)) {
1507 final var newAddress = resolveSignalServiceAddress(resolveRecipientTrusted(new SignalServiceAddress(
1508 registeredUsers.get(number),
1509 number)));
1510 signalServiceAddresses.add(newAddress);
1511 } else {
1512 signalServiceAddresses.add(address);
1513 }
1514 }
1515
1516 return signalServiceAddresses.stream().map(this::resolveRecipient).collect(Collectors.toSet());
1517 }
1518
1519 private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException {
1520 final var address = resolveSignalServiceAddress(recipientId);
1521 if (!address.getNumber().isPresent()) {
1522 return recipientId;
1523 }
1524 final var number = address.getNumber().get();
1525 final var uuidMap = getRegisteredUsers(Set.of(number));
1526 return resolveRecipientTrusted(new SignalServiceAddress(uuidMap.getOrDefault(number, null), number));
1527 }
1528
1529 private Map<String, UUID> getRegisteredUsers(final Set<String> numbers) throws IOException {
1530 try {
1531 return dependencies.getAccountManager()
1532 .getRegisteredUsers(ServiceConfig.getIasKeyStore(),
1533 numbers,
1534 serviceEnvironmentConfig.getCdsMrenclave());
1535 } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) {
1536 throw new IOException(e);
1537 }
1538 }
1539
1540 public void sendTypingMessage(
1541 TypingAction action, Set<String> recipients
1542 ) throws IOException, UntrustedIdentityException, InvalidNumberException {
1543 sendTypingMessageInternal(action, getSignalServiceAddresses(recipients));
1544 }
1545
1546 private void sendTypingMessageInternal(
1547 TypingAction action, Set<RecipientId> recipientIds
1548 ) throws IOException, UntrustedIdentityException {
1549 final var timestamp = System.currentTimeMillis();
1550 var message = new SignalServiceTypingMessage(action.toSignalService(), timestamp, Optional.absent());
1551 var messageSender = dependencies.getMessageSender();
1552 for (var recipientId : recipientIds) {
1553 final var address = resolveSignalServiceAddress(recipientId);
1554 messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message);
1555 }
1556 }
1557
1558 public void sendGroupTypingMessage(
1559 TypingAction action, GroupId groupId
1560 ) throws IOException, NotAGroupMemberException, GroupNotFoundException {
1561 final var timestamp = System.currentTimeMillis();
1562 final var g = getGroupForSending(groupId);
1563 final var message = new SignalServiceTypingMessage(action.toSignalService(),
1564 timestamp,
1565 Optional.of(groupId.serialize()));
1566 final var messageSender = dependencies.getMessageSender();
1567 final var recipientIdList = new ArrayList<>(g.getMembersWithout(account.getSelfRecipientId()));
1568 final var addresses = recipientIdList.stream()
1569 .map(this::resolveSignalServiceAddress)
1570 .collect(Collectors.toList());
1571 messageSender.sendTyping(addresses, unidentifiedAccessHelper.getAccessFor(recipientIdList), message, null);
1572 }
1573
1574 private Pair<Long, List<SendMessageResult>> sendMessage(
1575 SignalServiceDataMessage.Builder messageBuilder, Set<RecipientId> recipientIds
1576 ) throws IOException {
1577 final var timestamp = System.currentTimeMillis();
1578 messageBuilder.withTimestamp(timestamp);
1579
1580 SignalServiceDataMessage message = null;
1581 try {
1582 message = messageBuilder.build();
1583 if (message.getGroupContext().isPresent()) {
1584 try {
1585 var messageSender = dependencies.getMessageSender();
1586 final var isRecipientUpdate = false;
1587 final var recipientIdList = new ArrayList<>(recipientIds);
1588 final var addresses = recipientIdList.stream()
1589 .map(this::resolveSignalServiceAddress)
1590 .collect(Collectors.toList());
1591 var result = messageSender.sendDataMessage(addresses,
1592 unidentifiedAccessHelper.getAccessFor(recipientIdList),
1593 isRecipientUpdate,
1594 ContentHint.DEFAULT,
1595 message,
1596 sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()),
1597 () -> false);
1598
1599 for (var r : result) {
1600 if (r.getIdentityFailure() != null) {
1601 final var recipientId = resolveRecipient(r.getAddress());
1602 final var newIdentity = account.getIdentityKeyStore()
1603 .saveIdentity(recipientId, r.getIdentityFailure().getIdentityKey(), new Date());
1604 if (newIdentity) {
1605 account.getSessionStore().archiveSessions(recipientId);
1606 }
1607 }
1608 }
1609
1610 return new Pair<>(timestamp, result);
1611 } catch (UntrustedIdentityException e) {
1612 return new Pair<>(timestamp, List.of());
1613 }
1614 } else {
1615 // Send to all individually, so sync messages are sent correctly
1616 messageBuilder.withProfileKey(account.getProfileKey().serialize());
1617 var results = new ArrayList<SendMessageResult>(recipientIds.size());
1618 for (var recipientId : recipientIds) {
1619 final var contact = account.getContactStore().getContact(recipientId);
1620 final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
1621 messageBuilder.withExpiration(expirationTime);
1622 message = messageBuilder.build();
1623 results.add(sendMessage(recipientId, message));
1624 }
1625 return new Pair<>(timestamp, results);
1626 }
1627 } finally {
1628 if (message != null && message.isEndSession()) {
1629 for (var recipient : recipientIds) {
1630 handleEndSession(recipient);
1631 }
1632 }
1633 }
1634 }
1635
1636 private Pair<Long, SendMessageResult> sendSelfMessage(
1637 SignalServiceDataMessage.Builder messageBuilder
1638 ) throws IOException {
1639 final var timestamp = System.currentTimeMillis();
1640 messageBuilder.withTimestamp(timestamp);
1641 final var recipientId = account.getSelfRecipientId();
1642
1643 final var contact = account.getContactStore().getContact(recipientId);
1644 final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
1645 messageBuilder.withExpiration(expirationTime);
1646
1647 var message = messageBuilder.build();
1648 final var result = sendSelfMessage(message);
1649 return new Pair<>(timestamp, result);
1650 }
1651
1652 private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException {
1653 var messageSender = dependencies.getMessageSender();
1654
1655 var recipientId = account.getSelfRecipientId();
1656
1657 final var unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(recipientId);
1658 var recipient = resolveSignalServiceAddress(recipientId);
1659 var transcript = new SentTranscriptMessage(Optional.of(recipient),
1660 message.getTimestamp(),
1661 message,
1662 message.getExpiresInSeconds(),
1663 Map.of(recipient, unidentifiedAccess.isPresent()),
1664 false);
1665 var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
1666
1667 try {
1668 return messageSender.sendSyncMessage(syncMessage, unidentifiedAccess);
1669 } catch (UntrustedIdentityException e) {
1670 return SendMessageResult.identityFailure(recipient, e.getIdentityKey());
1671 }
1672 }
1673
1674 private SendMessageResult sendMessage(
1675 RecipientId recipientId, SignalServiceDataMessage message
1676 ) throws IOException {
1677 var messageSender = dependencies.getMessageSender();
1678
1679 final var address = resolveSignalServiceAddress(recipientId);
1680 try {
1681 try {
1682 return messageSender.sendDataMessage(address,
1683 unidentifiedAccessHelper.getAccessFor(recipientId),
1684 ContentHint.DEFAULT,
1685 message);
1686 } catch (UnregisteredUserException e) {
1687 final var newRecipientId = refreshRegisteredUser(recipientId);
1688 return messageSender.sendDataMessage(resolveSignalServiceAddress(newRecipientId),
1689 unidentifiedAccessHelper.getAccessFor(newRecipientId),
1690 ContentHint.DEFAULT,
1691 message);
1692 }
1693 } catch (UntrustedIdentityException e) {
1694 return SendMessageResult.identityFailure(address, e.getIdentityKey());
1695 }
1696 }
1697
1698 private SendMessageResult sendNullMessage(RecipientId recipientId) throws IOException {
1699 var messageSender = dependencies.getMessageSender();
1700
1701 final var address = resolveSignalServiceAddress(recipientId);
1702 try {
1703 try {
1704 return messageSender.sendNullMessage(address, unidentifiedAccessHelper.getAccessFor(recipientId));
1705 } catch (UnregisteredUserException e) {
1706 final var newRecipientId = refreshRegisteredUser(recipientId);
1707 final var newAddress = resolveSignalServiceAddress(newRecipientId);
1708 return messageSender.sendNullMessage(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId));
1709 }
1710 } catch (UntrustedIdentityException e) {
1711 return SendMessageResult.identityFailure(address, e.getIdentityKey());
1712 }
1713 }
1714
1715 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException, InvalidMessageStructureException {
1716 return dependencies.getCipher().decrypt(envelope);
1717 }
1718
1719 private void handleEndSession(RecipientId recipientId) {
1720 account.getSessionStore().deleteAllSessions(recipientId);
1721 }
1722
1723 private List<HandleAction> handleSignalServiceDataMessage(
1724 SignalServiceDataMessage message,
1725 boolean isSync,
1726 SignalServiceAddress source,
1727 SignalServiceAddress destination,
1728 boolean ignoreAttachments
1729 ) {
1730 var actions = new ArrayList<HandleAction>();
1731 if (message.getGroupContext().isPresent()) {
1732 if (message.getGroupContext().get().getGroupV1().isPresent()) {
1733 var groupInfo = message.getGroupContext().get().getGroupV1().get();
1734 var groupId = GroupId.v1(groupInfo.getGroupId());
1735 var group = getGroup(groupId);
1736 if (group == null || group instanceof GroupInfoV1) {
1737 var groupV1 = (GroupInfoV1) group;
1738 switch (groupInfo.getType()) {
1739 case UPDATE: {
1740 if (groupV1 == null) {
1741 groupV1 = new GroupInfoV1(groupId);
1742 }
1743
1744 if (groupInfo.getAvatar().isPresent()) {
1745 var avatar = groupInfo.getAvatar().get();
1746 downloadGroupAvatar(avatar, groupV1.getGroupId());
1747 }
1748
1749 if (groupInfo.getName().isPresent()) {
1750 groupV1.name = groupInfo.getName().get();
1751 }
1752
1753 if (groupInfo.getMembers().isPresent()) {
1754 groupV1.addMembers(groupInfo.getMembers()
1755 .get()
1756 .stream()
1757 .map(this::resolveRecipient)
1758 .collect(Collectors.toSet()));
1759 }
1760
1761 account.getGroupStore().updateGroup(groupV1);
1762 break;
1763 }
1764 case DELIVER:
1765 if (groupV1 == null && !isSync) {
1766 actions.add(new SendGroupInfoRequestAction(source, groupId));
1767 }
1768 break;
1769 case QUIT: {
1770 if (groupV1 != null) {
1771 groupV1.removeMember(resolveRecipient(source));
1772 account.getGroupStore().updateGroup(groupV1);
1773 }
1774 break;
1775 }
1776 case REQUEST_INFO:
1777 if (groupV1 != null && !isSync) {
1778 actions.add(new SendGroupInfoAction(source, groupV1.getGroupId()));
1779 }
1780 break;
1781 }
1782 } else {
1783 // Received a group v1 message for a v2 group
1784 }
1785 }
1786 if (message.getGroupContext().get().getGroupV2().isPresent()) {
1787 final var groupContext = message.getGroupContext().get().getGroupV2().get();
1788 final var groupMasterKey = groupContext.getMasterKey();
1789
1790 getOrMigrateGroup(groupMasterKey,
1791 groupContext.getRevision(),
1792 groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null);
1793 }
1794 }
1795
1796 final var conversationPartnerAddress = isSync ? destination : source;
1797 if (conversationPartnerAddress != null && message.isEndSession()) {
1798 handleEndSession(resolveRecipient(conversationPartnerAddress));
1799 }
1800 if (message.isExpirationUpdate() || message.getBody().isPresent()) {
1801 if (message.getGroupContext().isPresent()) {
1802 if (message.getGroupContext().get().getGroupV1().isPresent()) {
1803 var groupInfo = message.getGroupContext().get().getGroupV1().get();
1804 var group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId()));
1805 if (group != null) {
1806 if (group.messageExpirationTime != message.getExpiresInSeconds()) {
1807 group.messageExpirationTime = message.getExpiresInSeconds();
1808 account.getGroupStore().updateGroup(group);
1809 }
1810 }
1811 } else if (message.getGroupContext().get().getGroupV2().isPresent()) {
1812 // disappearing message timer already stored in the DecryptedGroup
1813 }
1814 } else if (conversationPartnerAddress != null) {
1815 setExpirationTimer(resolveRecipient(conversationPartnerAddress), message.getExpiresInSeconds());
1816 }
1817 }
1818 if (!ignoreAttachments) {
1819 if (message.getAttachments().isPresent()) {
1820 for (var attachment : message.getAttachments().get()) {
1821 downloadAttachment(attachment);
1822 }
1823 }
1824 if (message.getSharedContacts().isPresent()) {
1825 for (var contact : message.getSharedContacts().get()) {
1826 if (contact.getAvatar().isPresent()) {
1827 downloadAttachment(contact.getAvatar().get().getAttachment());
1828 }
1829 }
1830 }
1831 }
1832 if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
1833 final ProfileKey profileKey;
1834 try {
1835 profileKey = new ProfileKey(message.getProfileKey().get());
1836 } catch (InvalidInputException e) {
1837 throw new AssertionError(e);
1838 }
1839 if (source.matches(account.getSelfAddress())) {
1840 this.account.setProfileKey(profileKey);
1841 }
1842 this.account.getProfileStore().storeProfileKey(resolveRecipient(source), profileKey);
1843 }
1844 if (message.getPreviews().isPresent()) {
1845 final var previews = message.getPreviews().get();
1846 for (var preview : previews) {
1847 if (preview.getImage().isPresent()) {
1848 downloadAttachment(preview.getImage().get());
1849 }
1850 }
1851 }
1852 if (message.getQuote().isPresent()) {
1853 final var quote = message.getQuote().get();
1854
1855 for (var quotedAttachment : quote.getAttachments()) {
1856 final var thumbnail = quotedAttachment.getThumbnail();
1857 if (thumbnail != null) {
1858 downloadAttachment(thumbnail);
1859 }
1860 }
1861 }
1862 if (message.getSticker().isPresent()) {
1863 final var messageSticker = message.getSticker().get();
1864 final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId());
1865 var sticker = account.getStickerStore().getSticker(stickerPackId);
1866 if (sticker == null) {
1867 sticker = new Sticker(stickerPackId, messageSticker.getPackKey());
1868 account.getStickerStore().updateSticker(sticker);
1869 }
1870 enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey()));
1871 }
1872 return actions;
1873 }
1874
1875 private GroupInfoV2 getOrMigrateGroup(
1876 final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange
1877 ) {
1878 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
1879
1880 var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
1881 var groupInfo = getGroup(groupId);
1882 final GroupInfoV2 groupInfoV2;
1883 if (groupInfo instanceof GroupInfoV1) {
1884 // Received a v2 group message for a v1 group, we need to locally migrate the group
1885 account.getGroupStore().deleteGroupV1(((GroupInfoV1) groupInfo).getGroupId());
1886 groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
1887 logger.info("Locally migrated group {} to group v2, id: {}",
1888 groupInfo.getGroupId().toBase64(),
1889 groupInfoV2.getGroupId().toBase64());
1890 } else if (groupInfo instanceof GroupInfoV2) {
1891 groupInfoV2 = (GroupInfoV2) groupInfo;
1892 } else {
1893 groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
1894 }
1895
1896 if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < revision) {
1897 DecryptedGroup group = null;
1898 if (signedGroupChange != null
1899 && groupInfoV2.getGroup() != null
1900 && groupInfoV2.getGroup().getRevision() + 1 == revision) {
1901 group = groupV2Helper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(),
1902 signedGroupChange,
1903 groupMasterKey);
1904 }
1905 if (group == null) {
1906 group = groupV2Helper.getDecryptedGroup(groupSecretParams);
1907 }
1908 if (group != null) {
1909 storeProfileKeysFromMembers(group);
1910 final var avatar = group.getAvatar();
1911 if (avatar != null && !avatar.isEmpty()) {
1912 downloadGroupAvatar(groupId, groupSecretParams, avatar);
1913 }
1914 }
1915 groupInfoV2.setGroup(group, this::resolveRecipient);
1916 account.getGroupStore().updateGroup(groupInfoV2);
1917 }
1918
1919 return groupInfoV2;
1920 }
1921
1922 private void storeProfileKeysFromMembers(final DecryptedGroup group) {
1923 for (var member : group.getMembersList()) {
1924 final var uuid = UuidUtil.parseOrThrow(member.getUuid().toByteArray());
1925 final var recipientId = account.getRecipientStore().resolveRecipient(uuid);
1926 try {
1927 account.getProfileStore()
1928 .storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray()));
1929 } catch (InvalidInputException ignored) {
1930 }
1931 }
1932 }
1933
1934 private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
1935 Set<HandleAction> queuedActions = new HashSet<>();
1936 for (var cachedMessage : account.getMessageCache().getCachedMessages()) {
1937 var actions = retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage);
1938 if (actions != null) {
1939 queuedActions.addAll(actions);
1940 }
1941 }
1942 for (var action : queuedActions) {
1943 try {
1944 action.execute(this);
1945 } catch (Throwable e) {
1946 if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) {
1947 Thread.currentThread().interrupt();
1948 }
1949 logger.warn("Message action failed.", e);
1950 }
1951 }
1952 }
1953
1954 private List<HandleAction> retryFailedReceivedMessage(
1955 final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage
1956 ) {
1957 var envelope = cachedMessage.loadEnvelope();
1958 if (envelope == null) {
1959 return null;
1960 }
1961 SignalServiceContent content = null;
1962 List<HandleAction> actions = null;
1963 if (!envelope.isReceipt()) {
1964 try {
1965 content = decryptMessage(envelope);
1966 } catch (ProtocolUntrustedIdentityException e) {
1967 if (!envelope.hasSource()) {
1968 final var identifier = e.getSender();
1969 final var recipientId = resolveRecipient(identifier);
1970 try {
1971 account.getMessageCache().replaceSender(cachedMessage, recipientId);
1972 } catch (IOException ioException) {
1973 logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage());
1974 }
1975 }
1976 return null;
1977 } catch (Exception er) {
1978 // All other errors are not recoverable, so delete the cached message
1979 cachedMessage.delete();
1980 return null;
1981 }
1982 actions = handleMessage(envelope, content, ignoreAttachments);
1983 }
1984 handler.handleMessage(envelope, content, null);
1985 cachedMessage.delete();
1986 return actions;
1987 }
1988
1989 public void receiveMessages(
1990 long timeout,
1991 TimeUnit unit,
1992 boolean returnOnTimeout,
1993 boolean ignoreAttachments,
1994 ReceiveMessageHandler handler
1995 ) throws IOException, InterruptedException {
1996 retryFailedReceivedMessages(handler, ignoreAttachments);
1997
1998 Set<HandleAction> queuedActions = null;
1999
2000 final var signalWebSocket = dependencies.getSignalWebSocket();
2001 signalWebSocket.connect();
2002
2003 var hasCaughtUpWithOldMessages = false;
2004
2005 while (!Thread.interrupted()) {
2006 SignalServiceEnvelope envelope;
2007 SignalServiceContent content = null;
2008 Exception exception = null;
2009 final CachedMessage[] cachedMessage = {null};
2010 account.setLastReceiveTimestamp(System.currentTimeMillis());
2011 logger.debug("Checking for new message from server");
2012 try {
2013 var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> {
2014 final var recipientId = envelope1.hasSource()
2015 ? resolveRecipient(envelope1.getSourceIdentifier())
2016 : null;
2017 // store message on disk, before acknowledging receipt to the server
2018 cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
2019 });
2020 logger.debug("New message received from server");
2021 if (result.isPresent()) {
2022 envelope = result.get();
2023 } else {
2024 // Received indicator that server queue is empty
2025 hasCaughtUpWithOldMessages = true;
2026
2027 if (queuedActions != null) {
2028 for (var action : queuedActions) {
2029 try {
2030 action.execute(this);
2031 } catch (Throwable e) {
2032 if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) {
2033 Thread.currentThread().interrupt();
2034 }
2035 logger.warn("Message action failed.", e);
2036 }
2037 }
2038 queuedActions.clear();
2039 queuedActions = null;
2040 }
2041
2042 // Continue to wait another timeout for new messages
2043 continue;
2044 }
2045 } catch (AssertionError e) {
2046 if (e.getCause() instanceof InterruptedException) {
2047 throw (InterruptedException) e.getCause();
2048 } else {
2049 throw e;
2050 }
2051 } catch (WebSocketUnavailableException e) {
2052 logger.debug("Pipe unexpectedly unavailable, connecting");
2053 signalWebSocket.connect();
2054 continue;
2055 } catch (TimeoutException e) {
2056 if (returnOnTimeout) return;
2057 continue;
2058 }
2059
2060 if (envelope.hasSource()) {
2061 // Store uuid if we don't have it already
2062 // address/uuid in envelope is sent by server
2063 resolveRecipientTrusted(envelope.getSourceAddress());
2064 }
2065 final var notAGroupMember = isNotAGroupMember(envelope, content);
2066 if (!envelope.isReceipt()) {
2067 try {
2068 content = decryptMessage(envelope);
2069 } catch (Exception e) {
2070 exception = e;
2071 }
2072 if (!envelope.hasSource() && content != null) {
2073 // Store uuid if we don't have it already
2074 // address/uuid is validated by unidentified sender certificate
2075 resolveRecipientTrusted(content.getSender());
2076 }
2077 var actions = handleMessage(envelope, content, ignoreAttachments);
2078 if (exception instanceof ProtocolInvalidMessageException) {
2079 final var sender = resolveRecipient(((ProtocolInvalidMessageException) exception).getSender());
2080 logger.debug("Received invalid message, queuing renew session action.");
2081 actions.add(new RenewSessionAction(sender));
2082 }
2083 if (hasCaughtUpWithOldMessages) {
2084 for (var action : actions) {
2085 try {
2086 action.execute(this);
2087 } catch (Throwable e) {
2088 if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) {
2089 Thread.currentThread().interrupt();
2090 }
2091 logger.warn("Message action failed.", e);
2092 }
2093 }
2094 } else {
2095 if (queuedActions == null) {
2096 queuedActions = new HashSet<>();
2097 }
2098 queuedActions.addAll(actions);
2099 }
2100 }
2101 if (isMessageBlocked(envelope, content)) {
2102 logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp());
2103 } else if (notAGroupMember) {
2104 logger.info("Ignoring a message from a non group member: {}", envelope.getTimestamp());
2105 } else {
2106 handler.handleMessage(envelope, content, exception);
2107 }
2108 if (cachedMessage[0] != null) {
2109 if (exception instanceof ProtocolUntrustedIdentityException) {
2110 final var identifier = ((ProtocolUntrustedIdentityException) exception).getSender();
2111 final var recipientId = resolveRecipient(identifier);
2112 queuedActions.add(new RetrieveProfileAction(recipientId));
2113 if (!envelope.hasSource()) {
2114 try {
2115 cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId);
2116 } catch (IOException ioException) {
2117 logger.warn("Failed to move cached message to recipient folder: {}",
2118 ioException.getMessage());
2119 }
2120 }
2121 } else {
2122 cachedMessage[0].delete();
2123 }
2124 }
2125 }
2126 }
2127
2128 private boolean isMessageBlocked(
2129 SignalServiceEnvelope envelope, SignalServiceContent content
2130 ) {
2131 SignalServiceAddress source;
2132 if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
2133 source = envelope.getSourceAddress();
2134 } else if (content != null) {
2135 source = content.getSender();
2136 } else {
2137 return false;
2138 }
2139 final var recipientId = resolveRecipient(source);
2140 if (isContactBlocked(recipientId)) {
2141 return true;
2142 }
2143
2144 if (content != null && content.getDataMessage().isPresent()) {
2145 var message = content.getDataMessage().get();
2146 if (message.getGroupContext().isPresent()) {
2147 var groupId = GroupUtils.getGroupId(message.getGroupContext().get());
2148 var group = getGroup(groupId);
2149 if (group != null && group.isBlocked()) {
2150 return true;
2151 }
2152 }
2153 }
2154 return false;
2155 }
2156
2157 public boolean isContactBlocked(final String identifier) throws InvalidNumberException {
2158 final var recipientId = canonicalizeAndResolveRecipient(identifier);
2159 return isContactBlocked(recipientId);
2160 }
2161
2162 private boolean isContactBlocked(final RecipientId recipientId) {
2163 var sourceContact = account.getContactStore().getContact(recipientId);
2164 return sourceContact != null && sourceContact.isBlocked();
2165 }
2166
2167 private boolean isNotAGroupMember(
2168 SignalServiceEnvelope envelope, SignalServiceContent content
2169 ) {
2170 SignalServiceAddress source;
2171 if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
2172 source = envelope.getSourceAddress();
2173 } else if (content != null) {
2174 source = content.getSender();
2175 } else {
2176 return false;
2177 }
2178
2179 if (content != null && content.getDataMessage().isPresent()) {
2180 var message = content.getDataMessage().get();
2181 if (message.getGroupContext().isPresent()) {
2182 if (message.getGroupContext().get().getGroupV1().isPresent()) {
2183 var groupInfo = message.getGroupContext().get().getGroupV1().get();
2184 if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) {
2185 return false;
2186 }
2187 }
2188 var groupId = GroupUtils.getGroupId(message.getGroupContext().get());
2189 var group = getGroup(groupId);
2190 if (group != null && !group.isMember(resolveRecipient(source))) {
2191 return true;
2192 }
2193 }
2194 }
2195 return false;
2196 }
2197
2198 private List<HandleAction> handleMessage(
2199 SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments
2200 ) {
2201 var actions = new ArrayList<HandleAction>();
2202 if (content != null) {
2203 final SignalServiceAddress sender;
2204 if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
2205 sender = envelope.getSourceAddress();
2206 } else {
2207 sender = content.getSender();
2208 }
2209
2210 if (content.getDataMessage().isPresent()) {
2211 var message = content.getDataMessage().get();
2212
2213 if (content.isNeedsReceipt()) {
2214 actions.add(new SendReceiptAction(sender, message.getTimestamp()));
2215 }
2216
2217 actions.addAll(handleSignalServiceDataMessage(message,
2218 false,
2219 sender,
2220 account.getSelfAddress(),
2221 ignoreAttachments));
2222 }
2223 if (content.getSyncMessage().isPresent()) {
2224 account.setMultiDevice(true);
2225 var syncMessage = content.getSyncMessage().get();
2226 if (syncMessage.getSent().isPresent()) {
2227 var message = syncMessage.getSent().get();
2228 final var destination = message.getDestination().orNull();
2229 actions.addAll(handleSignalServiceDataMessage(message.getMessage(),
2230 true,
2231 sender,
2232 destination,
2233 ignoreAttachments));
2234 }
2235 if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) {
2236 var rm = syncMessage.getRequest().get();
2237 if (rm.isContactsRequest()) {
2238 actions.add(SendSyncContactsAction.create());
2239 }
2240 if (rm.isGroupsRequest()) {
2241 actions.add(SendSyncGroupsAction.create());
2242 }
2243 if (rm.isBlockedListRequest()) {
2244 actions.add(SendSyncBlockedListAction.create());
2245 }
2246 // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest();
2247 }
2248 if (syncMessage.getGroups().isPresent()) {
2249 File tmpFile = null;
2250 try {
2251 tmpFile = IOUtils.createTempFile();
2252 final var groupsMessage = syncMessage.getGroups().get();
2253 try (var attachmentAsStream = retrieveAttachmentAsStream(groupsMessage.asPointer(), tmpFile)) {
2254 var s = new DeviceGroupsInputStream(attachmentAsStream);
2255 DeviceGroup g;
2256 while (true) {
2257 try {
2258 g = s.read();
2259 } catch (IOException e) {
2260 logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage());
2261 continue;
2262 }
2263 if (g == null) {
2264 break;
2265 }
2266 var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId()));
2267 if (syncGroup != null) {
2268 if (g.getName().isPresent()) {
2269 syncGroup.name = g.getName().get();
2270 }
2271 syncGroup.addMembers(g.getMembers()
2272 .stream()
2273 .map(this::resolveRecipient)
2274 .collect(Collectors.toSet()));
2275 if (!g.isActive()) {
2276 syncGroup.removeMember(account.getSelfRecipientId());
2277 } else {
2278 // Add ourself to the member set as it's marked as active
2279 syncGroup.addMembers(List.of(account.getSelfRecipientId()));
2280 }
2281 syncGroup.blocked = g.isBlocked();
2282 if (g.getColor().isPresent()) {
2283 syncGroup.color = g.getColor().get();
2284 }
2285
2286 if (g.getAvatar().isPresent()) {
2287 downloadGroupAvatar(g.getAvatar().get(), syncGroup.getGroupId());
2288 }
2289 syncGroup.archived = g.isArchived();
2290 account.getGroupStore().updateGroup(syncGroup);
2291 }
2292 }
2293 }
2294 } catch (Exception e) {
2295 logger.warn("Failed to handle received sync groups “{}”, ignoring: {}",
2296 tmpFile,
2297 e.getMessage());
2298 } finally {
2299 if (tmpFile != null) {
2300 try {
2301 Files.delete(tmpFile.toPath());
2302 } catch (IOException e) {
2303 logger.warn("Failed to delete received groups temp file “{}”, ignoring: {}",
2304 tmpFile,
2305 e.getMessage());
2306 }
2307 }
2308 }
2309 }
2310 if (syncMessage.getBlockedList().isPresent()) {
2311 final var blockedListMessage = syncMessage.getBlockedList().get();
2312 for (var address : blockedListMessage.getAddresses()) {
2313 setContactBlocked(resolveRecipient(address), true);
2314 }
2315 for (var groupId : blockedListMessage.getGroupIds()
2316 .stream()
2317 .map(GroupId::unknownVersion)
2318 .collect(Collectors.toSet())) {
2319 try {
2320 setGroupBlocked(groupId, true);
2321 } catch (GroupNotFoundException e) {
2322 logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}",
2323 groupId.toBase64());
2324 }
2325 }
2326 }
2327 if (syncMessage.getContacts().isPresent()) {
2328 File tmpFile = null;
2329 try {
2330 tmpFile = IOUtils.createTempFile();
2331 final var contactsMessage = syncMessage.getContacts().get();
2332 try (var attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream()
2333 .asPointer(), tmpFile)) {
2334 var s = new DeviceContactsInputStream(attachmentAsStream);
2335 DeviceContact c;
2336 while (true) {
2337 try {
2338 c = s.read();
2339 } catch (IOException e) {
2340 logger.warn("Sync contacts contained invalid contact, ignoring: {}",
2341 e.getMessage());
2342 continue;
2343 }
2344 if (c == null) {
2345 break;
2346 }
2347 if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
2348 account.setProfileKey(c.getProfileKey().get());
2349 }
2350 final var recipientId = resolveRecipientTrusted(c.getAddress());
2351 var contact = account.getContactStore().getContact(recipientId);
2352 final var builder = contact == null
2353 ? Contact.newBuilder()
2354 : Contact.newBuilder(contact);
2355 if (c.getName().isPresent()) {
2356 builder.withName(c.getName().get());
2357 }
2358 if (c.getColor().isPresent()) {
2359 builder.withColor(c.getColor().get());
2360 }
2361 if (c.getProfileKey().isPresent()) {
2362 account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
2363 }
2364 if (c.getVerified().isPresent()) {
2365 final var verifiedMessage = c.getVerified().get();
2366 account.getIdentityKeyStore()
2367 .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()),
2368 verifiedMessage.getIdentityKey(),
2369 TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
2370 }
2371 if (c.getExpirationTimer().isPresent()) {
2372 builder.withMessageExpirationTime(c.getExpirationTimer().get());
2373 }
2374 builder.withBlocked(c.isBlocked());
2375 builder.withArchived(c.isArchived());
2376 account.getContactStore().storeContact(recipientId, builder.build());
2377
2378 if (c.getAvatar().isPresent()) {
2379 downloadContactAvatar(c.getAvatar().get(), c.getAddress());
2380 }
2381 }
2382 }
2383 } catch (Exception e) {
2384 logger.warn("Failed to handle received sync contacts “{}”, ignoring: {}",
2385 tmpFile,
2386 e.getMessage());
2387 } finally {
2388 if (tmpFile != null) {
2389 try {
2390 Files.delete(tmpFile.toPath());
2391 } catch (IOException e) {
2392 logger.warn("Failed to delete received contacts temp file “{}”, ignoring: {}",
2393 tmpFile,
2394 e.getMessage());
2395 }
2396 }
2397 }
2398 }
2399 if (syncMessage.getVerified().isPresent()) {
2400 final var verifiedMessage = syncMessage.getVerified().get();
2401 account.getIdentityKeyStore()
2402 .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()),
2403 verifiedMessage.getIdentityKey(),
2404 TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
2405 }
2406 if (syncMessage.getStickerPackOperations().isPresent()) {
2407 final var stickerPackOperationMessages = syncMessage.getStickerPackOperations().get();
2408 for (var m : stickerPackOperationMessages) {
2409 if (!m.getPackId().isPresent()) {
2410 continue;
2411 }
2412 final var stickerPackId = StickerPackId.deserialize(m.getPackId().get());
2413 final var installed = !m.getType().isPresent()
2414 || m.getType().get() == StickerPackOperationMessage.Type.INSTALL;
2415
2416 var sticker = account.getStickerStore().getSticker(stickerPackId);
2417 if (m.getPackKey().isPresent()) {
2418 if (sticker == null) {
2419 sticker = new Sticker(stickerPackId, m.getPackKey().get());
2420 }
2421 if (installed) {
2422 enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get()));
2423 }
2424 }
2425
2426 if (sticker != null) {
2427 sticker.setInstalled(installed);
2428 account.getStickerStore().updateSticker(sticker);
2429 }
2430 }
2431 }
2432 if (syncMessage.getFetchType().isPresent()) {
2433 switch (syncMessage.getFetchType().get()) {
2434 case LOCAL_PROFILE:
2435 getRecipientProfile(account.getSelfRecipientId(), true);
2436 case STORAGE_MANIFEST:
2437 // TODO
2438 }
2439 }
2440 if (syncMessage.getKeys().isPresent()) {
2441 final var keysMessage = syncMessage.getKeys().get();
2442 if (keysMessage.getStorageService().isPresent()) {
2443 final var storageKey = keysMessage.getStorageService().get();
2444 account.setStorageKey(storageKey);
2445 }
2446 }
2447 if (syncMessage.getConfiguration().isPresent()) {
2448 // TODO
2449 }
2450 }
2451 }
2452 return actions;
2453 }
2454
2455 private void downloadContactAvatar(SignalServiceAttachment avatar, SignalServiceAddress address) {
2456 try {
2457 avatarStore.storeContactAvatar(address, outputStream -> retrieveAttachment(avatar, outputStream));
2458 } catch (IOException e) {
2459 logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage());
2460 }
2461 }
2462
2463 private void downloadGroupAvatar(SignalServiceAttachment avatar, GroupId groupId) {
2464 try {
2465 avatarStore.storeGroupAvatar(groupId, outputStream -> retrieveAttachment(avatar, outputStream));
2466 } catch (IOException e) {
2467 logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage());
2468 }
2469 }
2470
2471 private void downloadGroupAvatar(GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey) {
2472 try {
2473 avatarStore.storeGroupAvatar(groupId,
2474 outputStream -> retrieveGroupV2Avatar(groupSecretParams, cdnKey, outputStream));
2475 } catch (IOException e) {
2476 logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage());
2477 }
2478 }
2479
2480 private void downloadProfileAvatar(
2481 SignalServiceAddress address, String avatarPath, ProfileKey profileKey
2482 ) {
2483 try {
2484 avatarStore.storeProfileAvatar(address,
2485 outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream));
2486 } catch (Throwable e) {
2487 if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) {
2488 Thread.currentThread().interrupt();
2489 }
2490 logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage());
2491 }
2492 }
2493
2494 public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
2495 return attachmentStore.getAttachmentFile(attachmentId);
2496 }
2497
2498 private void downloadAttachment(final SignalServiceAttachment attachment) {
2499 if (!attachment.isPointer()) {
2500 logger.warn("Invalid state, can't store an attachment stream.");
2501 }
2502
2503 var pointer = attachment.asPointer();
2504 if (pointer.getPreview().isPresent()) {
2505 final var preview = pointer.getPreview().get();
2506 try {
2507 attachmentStore.storeAttachmentPreview(pointer.getRemoteId(),
2508 outputStream -> outputStream.write(preview, 0, preview.length));
2509 } catch (IOException e) {
2510 logger.warn("Failed to download attachment preview, ignoring: {}", e.getMessage());
2511 }
2512 }
2513
2514 try {
2515 attachmentStore.storeAttachment(pointer.getRemoteId(),
2516 outputStream -> retrieveAttachmentPointer(pointer, outputStream));
2517 } catch (IOException e) {
2518 logger.warn("Failed to download attachment ({}), ignoring: {}", pointer.getRemoteId(), e.getMessage());
2519 }
2520 }
2521
2522 private void retrieveGroupV2Avatar(
2523 GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream
2524 ) throws IOException {
2525 var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
2526
2527 var tmpFile = IOUtils.createTempFile();
2528 try (InputStream input = dependencies.getMessageReceiver()
2529 .retrieveGroupsV2ProfileAvatar(cdnKey, tmpFile, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
2530 var encryptedData = IOUtils.readFully(input);
2531
2532 var decryptedData = groupOperations.decryptAvatar(encryptedData);
2533 outputStream.write(decryptedData);
2534 } finally {
2535 try {
2536 Files.delete(tmpFile.toPath());
2537 } catch (IOException e) {
2538 logger.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}",
2539 tmpFile,
2540 e.getMessage());
2541 }
2542 }
2543 }
2544
2545 private void retrieveProfileAvatar(
2546 String avatarPath, ProfileKey profileKey, OutputStream outputStream
2547 ) throws IOException {
2548 var tmpFile = IOUtils.createTempFile();
2549 try (var input = dependencies.getMessageReceiver()
2550 .retrieveProfileAvatar(avatarPath,
2551 tmpFile,
2552 profileKey,
2553 ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
2554 // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
2555 IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
2556 } finally {
2557 try {
2558 Files.delete(tmpFile.toPath());
2559 } catch (IOException e) {
2560 logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}",
2561 tmpFile,
2562 e.getMessage());
2563 }
2564 }
2565 }
2566
2567 private void retrieveAttachment(
2568 final SignalServiceAttachment attachment, final OutputStream outputStream
2569 ) throws IOException {
2570 if (attachment.isPointer()) {
2571 var pointer = attachment.asPointer();
2572 retrieveAttachmentPointer(pointer, outputStream);
2573 } else {
2574 var stream = attachment.asStream();
2575 IOUtils.copyStream(stream.getInputStream(), outputStream);
2576 }
2577 }
2578
2579 private void retrieveAttachmentPointer(
2580 SignalServiceAttachmentPointer pointer, OutputStream outputStream
2581 ) throws IOException {
2582 var tmpFile = IOUtils.createTempFile();
2583 try (var input = retrieveAttachmentAsStream(pointer, tmpFile)) {
2584 IOUtils.copyStream(input, outputStream);
2585 } catch (MissingConfigurationException | InvalidMessageException e) {
2586 throw new IOException(e);
2587 } finally {
2588 try {
2589 Files.delete(tmpFile.toPath());
2590 } catch (IOException e) {
2591 logger.warn("Failed to delete received attachment temp file “{}”, ignoring: {}",
2592 tmpFile,
2593 e.getMessage());
2594 }
2595 }
2596 }
2597
2598 private InputStream retrieveAttachmentAsStream(
2599 SignalServiceAttachmentPointer pointer, File tmpFile
2600 ) throws IOException, InvalidMessageException, MissingConfigurationException {
2601 return dependencies.getMessageReceiver()
2602 .retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE);
2603 }
2604
2605 void sendGroups() throws IOException, UntrustedIdentityException {
2606 var groupsFile = IOUtils.createTempFile();
2607
2608 try {
2609 try (OutputStream fos = new FileOutputStream(groupsFile)) {
2610 var out = new DeviceGroupsOutputStream(fos);
2611 for (var record : getGroups()) {
2612 if (record instanceof GroupInfoV1) {
2613 var groupInfo = (GroupInfoV1) record;
2614 out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
2615 Optional.fromNullable(groupInfo.name),
2616 groupInfo.getMembers()
2617 .stream()
2618 .map(this::resolveSignalServiceAddress)
2619 .collect(Collectors.toList()),
2620 createGroupAvatarAttachment(groupInfo.getGroupId()),
2621 groupInfo.isMember(account.getSelfRecipientId()),
2622 Optional.of(groupInfo.messageExpirationTime),
2623 Optional.fromNullable(groupInfo.color),
2624 groupInfo.blocked,
2625 Optional.absent(),
2626 groupInfo.archived));
2627 }
2628 }
2629 }
2630
2631 if (groupsFile.exists() && groupsFile.length() > 0) {
2632 try (var groupsFileStream = new FileInputStream(groupsFile)) {
2633 var attachmentStream = SignalServiceAttachment.newStreamBuilder()
2634 .withStream(groupsFileStream)
2635 .withContentType("application/octet-stream")
2636 .withLength(groupsFile.length())
2637 .build();
2638
2639 sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
2640 }
2641 }
2642 } finally {
2643 try {
2644 Files.delete(groupsFile.toPath());
2645 } catch (IOException e) {
2646 logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage());
2647 }
2648 }
2649 }
2650
2651 public void sendContacts() throws IOException, UntrustedIdentityException {
2652 var contactsFile = IOUtils.createTempFile();
2653
2654 try {
2655 try (OutputStream fos = new FileOutputStream(contactsFile)) {
2656 var out = new DeviceContactsOutputStream(fos);
2657 for (var contactPair : account.getContactStore().getContacts()) {
2658 final var recipientId = contactPair.first();
2659 final var contact = contactPair.second();
2660 final var address = resolveSignalServiceAddress(recipientId);
2661
2662 var currentIdentity = account.getIdentityKeyStore().getIdentity(recipientId);
2663 VerifiedMessage verifiedMessage = null;
2664 if (currentIdentity != null) {
2665 verifiedMessage = new VerifiedMessage(address,
2666 currentIdentity.getIdentityKey(),
2667 currentIdentity.getTrustLevel().toVerifiedState(),
2668 currentIdentity.getDateAdded().getTime());
2669 }
2670
2671 var profileKey = account.getProfileStore().getProfileKey(recipientId);
2672 out.write(new DeviceContact(address,
2673 Optional.fromNullable(contact.getName()),
2674 createContactAvatarAttachment(address),
2675 Optional.fromNullable(contact.getColor()),
2676 Optional.fromNullable(verifiedMessage),
2677 Optional.fromNullable(profileKey),
2678 contact.isBlocked(),
2679 Optional.of(contact.getMessageExpirationTime()),
2680 Optional.absent(),
2681 contact.isArchived()));
2682 }
2683
2684 if (account.getProfileKey() != null) {
2685 // Send our own profile key as well
2686 out.write(new DeviceContact(account.getSelfAddress(),
2687 Optional.absent(),
2688 Optional.absent(),
2689 Optional.absent(),
2690 Optional.absent(),
2691 Optional.of(account.getProfileKey()),
2692 false,
2693 Optional.absent(),
2694 Optional.absent(),
2695 false));
2696 }
2697 }
2698
2699 if (contactsFile.exists() && contactsFile.length() > 0) {
2700 try (var contactsFileStream = new FileInputStream(contactsFile)) {
2701 var attachmentStream = SignalServiceAttachment.newStreamBuilder()
2702 .withStream(contactsFileStream)
2703 .withContentType("application/octet-stream")
2704 .withLength(contactsFile.length())
2705 .build();
2706
2707 sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, true)));
2708 }
2709 }
2710 } finally {
2711 try {
2712 Files.delete(contactsFile.toPath());
2713 } catch (IOException e) {
2714 logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage());
2715 }
2716 }
2717 }
2718
2719 void sendBlockedList() throws IOException, UntrustedIdentityException {
2720 var addresses = new ArrayList<SignalServiceAddress>();
2721 for (var record : account.getContactStore().getContacts()) {
2722 if (record.second().isBlocked()) {
2723 addresses.add(resolveSignalServiceAddress(record.first()));
2724 }
2725 }
2726 var groupIds = new ArrayList<byte[]>();
2727 for (var record : getGroups()) {
2728 if (record.isBlocked()) {
2729 groupIds.add(record.getGroupId().serialize());
2730 }
2731 }
2732 sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
2733 }
2734
2735 private void sendVerifiedMessage(
2736 SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel
2737 ) throws IOException, UntrustedIdentityException {
2738 var verifiedMessage = new VerifiedMessage(destination,
2739 identityKey,
2740 trustLevel.toVerifiedState(),
2741 System.currentTimeMillis());
2742 sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
2743 }
2744
2745 public List<Pair<RecipientId, Contact>> getContacts() {
2746 return account.getContactStore().getContacts();
2747 }
2748
2749 public String getContactOrProfileName(String number) throws InvalidNumberException {
2750 final var recipientId = canonicalizeAndResolveRecipient(number);
2751 final var recipient = account.getRecipientStore().getRecipient(recipientId);
2752 if (recipient == null) {
2753 return null;
2754 }
2755
2756 if (recipient.getContact() != null && !Util.isEmpty(recipient.getContact().getName())) {
2757 return recipient.getContact().getName();
2758 }
2759
2760 if (recipient.getProfile() != null && recipient.getProfile() != null) {
2761 return recipient.getProfile().getDisplayName();
2762 }
2763
2764 return null;
2765 }
2766
2767 public GroupInfo getGroup(GroupId groupId) {
2768 return getGroup(groupId, false);
2769 }
2770
2771 public GroupInfo getGroup(GroupId groupId, boolean forceUpdate) {
2772 final var group = account.getGroupStore().getGroup(groupId);
2773 if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) {
2774 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
2775 ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), this::resolveRecipient);
2776 account.getGroupStore().updateGroup(group);
2777 }
2778 return group;
2779 }
2780
2781 public List<IdentityInfo> getIdentities() {
2782 return account.getIdentityKeyStore().getIdentities();
2783 }
2784
2785 public List<IdentityInfo> getIdentities(String number) throws InvalidNumberException {
2786 final var identity = account.getIdentityKeyStore().getIdentity(canonicalizeAndResolveRecipient(number));
2787 return identity == null ? List.of() : List.of(identity);
2788 }
2789
2790 /**
2791 * Trust this the identity with this fingerprint
2792 *
2793 * @param name username of the identity
2794 * @param fingerprint Fingerprint
2795 */
2796 public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException {
2797 var recipientId = canonicalizeAndResolveRecipient(name);
2798 return trustIdentity(recipientId,
2799 identityKey -> Arrays.equals(identityKey.serialize(), fingerprint),
2800 TrustLevel.TRUSTED_VERIFIED);
2801 }
2802
2803 /**
2804 * Trust this the identity with this safety number
2805 *
2806 * @param name username of the identity
2807 * @param safetyNumber Safety number
2808 */
2809 public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException {
2810 var recipientId = canonicalizeAndResolveRecipient(name);
2811 var address = account.getRecipientStore().resolveServiceAddress(recipientId);
2812 return trustIdentity(recipientId,
2813 identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)),
2814 TrustLevel.TRUSTED_VERIFIED);
2815 }
2816
2817 /**
2818 * Trust all keys of this identity without verification
2819 *
2820 * @param name username of the identity
2821 */
2822 public boolean trustIdentityAllKeys(String name) throws InvalidNumberException {
2823 var recipientId = canonicalizeAndResolveRecipient(name);
2824 return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED);
2825 }
2826
2827 private boolean trustIdentity(
2828 RecipientId recipientId, Function<IdentityKey, Boolean> verifier, TrustLevel trustLevel
2829 ) {
2830 var identity = account.getIdentityKeyStore().getIdentity(recipientId);
2831 if (identity == null) {
2832 return false;
2833 }
2834
2835 if (!verifier.apply(identity.getIdentityKey())) {
2836 return false;
2837 }
2838
2839 account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel);
2840 try {
2841 var address = account.getRecipientStore().resolveServiceAddress(recipientId);
2842 sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel);
2843 } catch (IOException | UntrustedIdentityException e) {
2844 logger.warn("Failed to send verification sync message: {}", e.getMessage());
2845 }
2846
2847 return true;
2848 }
2849
2850 public String computeSafetyNumber(
2851 SignalServiceAddress theirAddress, IdentityKey theirIdentityKey
2852 ) {
2853 return Utils.computeSafetyNumber(ServiceConfig.capabilities.isUuid(),
2854 account.getSelfAddress(),
2855 getIdentityKeyPair().getPublicKey(),
2856 theirAddress,
2857 theirIdentityKey);
2858 }
2859
2860 @Deprecated
2861 public SignalServiceAddress resolveSignalServiceAddress(String identifier) {
2862 var address = Utils.getSignalServiceAddressFromIdentifier(identifier);
2863
2864 return resolveSignalServiceAddress(address);
2865 }
2866
2867 @Deprecated
2868 public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) {
2869 if (address.matches(account.getSelfAddress())) {
2870 return account.getSelfAddress();
2871 }
2872
2873 return account.getRecipientStore().resolveServiceAddress(address);
2874 }
2875
2876 public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) {
2877 return account.getRecipientStore().resolveServiceAddress(recipientId);
2878 }
2879
2880 public RecipientId canonicalizeAndResolveRecipient(String identifier) throws InvalidNumberException {
2881 var canonicalizedNumber = UuidUtil.isUuid(identifier)
2882 ? identifier
2883 : PhoneNumberFormatter.formatNumber(identifier, account.getUsername());
2884
2885 return resolveRecipient(canonicalizedNumber);
2886 }
2887
2888 private RecipientId resolveRecipient(final String identifier) {
2889 var address = Utils.getSignalServiceAddressFromIdentifier(identifier);
2890
2891 return resolveRecipient(address);
2892 }
2893
2894 public RecipientId resolveRecipient(SignalServiceAddress address) {
2895 return account.getRecipientStore().resolveRecipient(address);
2896 }
2897
2898 private RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
2899 return account.getRecipientStore().resolveRecipientTrusted(address);
2900 }
2901
2902 private void enqueueJob(Job job) {
2903 var context = new Context(account,
2904 dependencies.getAccountManager(),
2905 dependencies.getMessageReceiver(),
2906 stickerPackStore);
2907 job.run(context);
2908 }
2909
2910 @Override
2911 public void close() throws IOException {
2912 close(true);
2913 }
2914
2915 void close(boolean closeAccount) throws IOException {
2916 executor.shutdown();
2917
2918 dependencies.getSignalWebSocket().disconnect();
2919
2920 if (closeAccount && account != null) {
2921 account.close();
2922 }
2923 account = null;
2924 }
2925
2926 public interface ReceiveMessageHandler {
2927
2928 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e);
2929 }
2930 }