]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java
Add addStickerPack command
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / SendHelper.java
1 package org.asamk.signal.manager.helper;
2
3 import com.google.protobuf.ByteString;
4
5 import org.asamk.signal.manager.api.Contact;
6 import org.asamk.signal.manager.api.GroupId;
7 import org.asamk.signal.manager.api.GroupNotFoundException;
8 import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
9 import org.asamk.signal.manager.api.NotAGroupMemberException;
10 import org.asamk.signal.manager.api.Profile;
11 import org.asamk.signal.manager.api.UnregisteredRecipientException;
12 import org.asamk.signal.manager.groups.GroupUtils;
13 import org.asamk.signal.manager.internal.SignalDependencies;
14 import org.asamk.signal.manager.storage.SignalAccount;
15 import org.asamk.signal.manager.storage.groups.GroupInfo;
16 import org.asamk.signal.manager.storage.recipients.RecipientId;
17 import org.asamk.signal.manager.storage.sendLog.MessageSendLogEntry;
18 import org.signal.libsignal.protocol.InvalidKeyException;
19 import org.signal.libsignal.protocol.InvalidRegistrationIdException;
20 import org.signal.libsignal.protocol.NoSessionException;
21 import org.signal.libsignal.protocol.SignalProtocolAddress;
22 import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25 import org.whispersystems.signalservice.api.SignalServiceMessageSender;
26 import org.whispersystems.signalservice.api.crypto.ContentHint;
27 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
28 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
29 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
30 import org.whispersystems.signalservice.api.messages.SendMessageResult;
31 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
32 import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
33 import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
34 import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
35 import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
36 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
37 import org.whispersystems.signalservice.api.push.DistributionId;
38 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
39 import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
40 import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
41 import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
42 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
43 import org.whispersystems.signalservice.internal.push.exceptions.InvalidUnidentifiedAccessHeaderException;
44 import org.whispersystems.signalservice.internal.push.http.PartialSendCompleteListener;
45
46 import java.io.IOException;
47 import java.time.Duration;
48 import java.util.ArrayList;
49 import java.util.HashSet;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Optional;
53 import java.util.Set;
54 import java.util.concurrent.TimeUnit;
55 import java.util.concurrent.atomic.AtomicLong;
56
57 public class SendHelper {
58
59 private final static Logger logger = LoggerFactory.getLogger(SendHelper.class);
60
61 private final SignalAccount account;
62 private final SignalDependencies dependencies;
63 private final Context context;
64
65 public SendHelper(final Context context) {
66 this.account = context.getAccount();
67 this.dependencies = context.getDependencies();
68 this.context = context;
69 }
70
71 /**
72 * Send a single message to one recipient.
73 * The message is extended with the current expiration timer.
74 */
75 public SendMessageResult sendMessage(
76 final SignalServiceDataMessage.Builder messageBuilder,
77 final RecipientId recipientId,
78 Optional<Long> editTargetTimestamp
79 ) {
80 var contact = account.getContactStore().getContact(recipientId);
81 if (contact == null || !contact.isProfileSharingEnabled()) {
82 final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
83 contact = contactBuilder.withProfileSharingEnabled(true).build();
84 account.getContactStore().storeContact(recipientId, contact);
85 }
86
87 final var expirationTime = contact.getMessageExpirationTime();
88 messageBuilder.withExpiration(expirationTime);
89
90 if (!contact.isBlocked()) {
91 final var profileKey = account.getProfileKey().serialize();
92 messageBuilder.withProfileKey(profileKey);
93 }
94
95 final var message = messageBuilder.build();
96 return sendMessage(message, recipientId, editTargetTimestamp);
97 }
98
99 /**
100 * Send a group message to the given group
101 * The message is extended with the current expiration timer for the group and the group context.
102 */
103 public List<SendMessageResult> sendAsGroupMessage(
104 SignalServiceDataMessage.Builder messageBuilder, GroupId groupId, Optional<Long> editTargetTimestamp
105 ) throws IOException, GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException {
106 final var g = getGroupForSending(groupId);
107 return sendAsGroupMessage(messageBuilder, g, editTargetTimestamp);
108 }
109
110 /**
111 * Send a complete group message to the given recipients (should be current/old/new members)
112 * This method should only be used for create/update/quit group messages.
113 */
114 public List<SendMessageResult> sendGroupMessage(
115 final SignalServiceDataMessage message,
116 final Set<RecipientId> recipientIds,
117 final DistributionId distributionId
118 ) throws IOException {
119 return sendGroupMessage(message, recipientIds, distributionId, ContentHint.IMPLICIT, Optional.empty());
120 }
121
122 public SendMessageResult sendReceiptMessage(
123 final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId
124 ) {
125 final var messageSendLogStore = account.getMessageSendLogStore();
126 final var result = handleSendMessage(recipientId,
127 (messageSender, address, unidentifiedAccess) -> messageSender.sendReceipt(address,
128 unidentifiedAccess,
129 receiptMessage,
130 false));
131 messageSendLogStore.insertIfPossible(receiptMessage.getWhen(), result, ContentHint.IMPLICIT, false);
132 handleSendMessageResult(result);
133 return result;
134 }
135
136 public SendMessageResult sendProfileKey(RecipientId recipientId) {
137 logger.debug("Sending updated profile key to recipient: {}", recipientId);
138 final var profileKey = account.getProfileKey().serialize();
139 final var message = SignalServiceDataMessage.newBuilder()
140 .asProfileKeyUpdate(true)
141 .withProfileKey(profileKey)
142 .build();
143 return handleSendMessage(recipientId,
144 (messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address,
145 unidentifiedAccess,
146 ContentHint.IMPLICIT,
147 message,
148 SignalServiceMessageSender.IndividualSendEvents.EMPTY,
149 false,
150 false));
151 }
152
153 public SendMessageResult sendRetryReceipt(
154 DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional<GroupId> groupId
155 ) {
156 logger.debug("Sending retry receipt for {} to {}, device: {}",
157 errorMessage.getTimestamp(),
158 recipientId,
159 errorMessage.getDeviceId());
160 final var result = handleSendMessage(recipientId,
161 (messageSender, address, unidentifiedAccess) -> messageSender.sendRetryReceipt(address,
162 unidentifiedAccess,
163 groupId.map(GroupId::serialize),
164 errorMessage));
165 handleSendMessageResult(result);
166 return result;
167 }
168
169 public SendMessageResult sendNullMessage(RecipientId recipientId) {
170 final var result = handleSendMessage(recipientId, SignalServiceMessageSender::sendNullMessage);
171 handleSendMessageResult(result);
172 return result;
173 }
174
175 public SendMessageResult sendSelfMessage(
176 SignalServiceDataMessage.Builder messageBuilder, Optional<Long> editTargetTimestamp
177 ) {
178 final var recipientId = account.getSelfRecipientId();
179 final var contact = account.getContactStore().getContact(recipientId);
180 final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
181 messageBuilder.withExpiration(expirationTime);
182
183 var message = messageBuilder.build();
184 return sendSelfMessage(message, editTargetTimestamp);
185 }
186
187 public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message) {
188 var messageSender = dependencies.getMessageSender();
189 if (!account.isMultiDevice()) {
190 logger.trace("Not sending sync message because there are no linked devices.");
191 return SendMessageResult.success(account.getSelfAddress(), List.of(), false, false, 0, Optional.empty());
192 }
193 try {
194 return messageSender.sendSyncMessage(message, context.getUnidentifiedAccessHelper().getAccessForSync());
195 } catch (UnregisteredUserException e) {
196 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
197 return SendMessageResult.unregisteredFailure(address);
198 } catch (ProofRequiredException e) {
199 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
200 return SendMessageResult.proofRequiredFailure(address, e);
201 } catch (RateLimitException e) {
202 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
203 logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage());
204 return SendMessageResult.rateLimitFailure(address, e);
205 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
206 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
207 return SendMessageResult.identityFailure(address, e.getIdentityKey());
208 } catch (IOException e) {
209 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
210 logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
211 logger.debug("Exception", e);
212 return SendMessageResult.networkFailure(address);
213 }
214 }
215
216 public SendMessageResult sendTypingMessage(
217 SignalServiceTypingMessage message, RecipientId recipientId
218 ) {
219 final var result = handleSendMessage(recipientId,
220 (messageSender, address, unidentifiedAccess) -> messageSender.sendTyping(List.of(address),
221 List.of(unidentifiedAccess),
222 message,
223 null).get(0));
224 handleSendMessageResult(result);
225 return result;
226 }
227
228 public List<SendMessageResult> sendGroupTypingMessage(
229 SignalServiceTypingMessage message, GroupId groupId
230 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
231 final var g = getGroupForSending(groupId);
232 if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
233 throw new GroupSendingNotAllowedException(groupId, g.getTitle());
234 }
235 final var distributionId = g.getDistributionId();
236 final var recipientIds = g.getMembersWithout(account.getSelfRecipientId());
237
238 return sendGroupTypingMessage(message, recipientIds, distributionId);
239 }
240
241 public SendMessageResult resendMessage(
242 final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry
243 ) {
244 logger.trace("Resending message {} to {}", timestamp, recipientId);
245 if (messageSendLogEntry.groupId().isEmpty()) {
246 return handleSendMessage(recipientId,
247 (messageSender, address, unidentifiedAccess) -> messageSender.resendContent(address,
248 unidentifiedAccess,
249 timestamp,
250 messageSendLogEntry.content(),
251 messageSendLogEntry.contentHint(),
252 Optional.empty(),
253 messageSendLogEntry.urgent()));
254 }
255
256 final var groupId = messageSendLogEntry.groupId().get();
257 final var group = account.getGroupStore().getGroup(groupId);
258
259 if (group == null) {
260 logger.debug("Could not find a matching group for the groupId {}! Skipping message send.",
261 groupId.toBase64());
262 return null;
263 } else if (!group.getMembers().contains(recipientId)) {
264 logger.warn("The target user is no longer in the group {}! Skipping message send.", groupId.toBase64());
265 return null;
266 }
267
268 final var senderKeyDistributionMessage = dependencies.getMessageSender()
269 .getOrCreateNewGroupSession(group.getDistributionId());
270 final var distributionBytes = ByteString.copyFrom(senderKeyDistributionMessage.serialize());
271 final var contentToSend = messageSendLogEntry.content()
272 .toBuilder()
273 .setSenderKeyDistributionMessage(distributionBytes)
274 .build();
275
276 final var result = handleSendMessage(recipientId,
277 (messageSender, address, unidentifiedAccess) -> messageSender.resendContent(address,
278 unidentifiedAccess,
279 timestamp,
280 contentToSend,
281 messageSendLogEntry.contentHint(),
282 Optional.of(group.getGroupId().serialize()),
283 messageSendLogEntry.urgent()));
284
285 if (result.isSuccess()) {
286 final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
287 final var addresses = result.getSuccess()
288 .getDevices()
289 .stream()
290 .map(device -> new SignalProtocolAddress(address.getIdentifier(), device))
291 .toList();
292
293 account.getSenderKeyStore().markSenderKeySharedWith(group.getDistributionId(), addresses);
294 }
295
296 return result;
297 }
298
299 private List<SendMessageResult> sendAsGroupMessage(
300 final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g, Optional<Long> editTargetTimestamp
301 ) throws IOException, GroupSendingNotAllowedException {
302 GroupUtils.setGroupContext(messageBuilder, g);
303 messageBuilder.withExpiration(g.getMessageExpirationTimer());
304
305 final var message = messageBuilder.build();
306 final var recipients = g.getMembersWithout(account.getSelfRecipientId());
307
308 if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
309 if (message.getBody().isPresent()
310 || message.getAttachments().isPresent()
311 || message.getQuote().isPresent()
312 || message.getPreviews().isPresent()
313 || message.getMentions().isPresent()
314 || message.getSticker().isPresent()) {
315 throw new GroupSendingNotAllowedException(g.getGroupId(), g.getTitle());
316 }
317 }
318
319 return sendGroupMessage(message,
320 recipients,
321 g.getDistributionId(),
322 ContentHint.RESENDABLE,
323 editTargetTimestamp);
324 }
325
326 private List<SendMessageResult> sendGroupMessage(
327 final SignalServiceDataMessage message,
328 final Set<RecipientId> recipientIds,
329 final DistributionId distributionId,
330 final ContentHint contentHint,
331 final Optional<Long> editTargetTimestamp
332 ) throws IOException {
333 final var messageSender = dependencies.getMessageSender();
334 final var messageSendLogStore = account.getMessageSendLogStore();
335 final AtomicLong entryId = new AtomicLong(-1);
336
337 final var urgent = true;
338 final PartialSendCompleteListener partialSendCompleteListener = sendResult -> {
339 logger.trace("Partial message send result: {}", sendResult.isSuccess());
340 synchronized (entryId) {
341 if (entryId.get() == -1) {
342 final var newId = messageSendLogStore.insertIfPossible(message.getTimestamp(),
343 sendResult,
344 contentHint,
345 urgent);
346 entryId.set(newId);
347 } else {
348 messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
349 }
350 }
351 };
352 final LegacySenderHandler legacySender = (recipients, unidentifiedAccess, isRecipientUpdate) ->
353 editTargetTimestamp.isEmpty()
354 ? messageSender.sendDataMessage(recipients,
355 unidentifiedAccess,
356 isRecipientUpdate,
357 contentHint,
358 message,
359 SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
360 partialSendCompleteListener,
361 () -> false,
362 urgent)
363 : messageSender.sendEditMessage(recipients,
364 unidentifiedAccess,
365 isRecipientUpdate,
366 contentHint,
367 message,
368 SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
369 partialSendCompleteListener,
370 () -> false,
371 urgent,
372 editTargetTimestamp.get());
373 final SenderKeySenderHandler senderKeySender = (distId, recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendGroupDataMessage(
374 distId,
375 recipients,
376 unidentifiedAccess,
377 isRecipientUpdate,
378 contentHint,
379 message,
380 SignalServiceMessageSender.SenderKeyGroupEvents.EMPTY,
381 urgent,
382 false,
383 editTargetTimestamp.map(timestamp -> new SignalServiceEditMessage(timestamp, message)).orElse(null),
384 sendResult -> {
385 logger.trace("Partial message send results: {}", sendResult.size());
386 synchronized (entryId) {
387 if (entryId.get() == -1) {
388 final var newId = messageSendLogStore.insertIfPossible(message.getTimestamp(),
389 sendResult,
390 contentHint,
391 urgent);
392 entryId.set(newId);
393 } else {
394 messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
395 }
396 }
397 synchronized (entryId) {
398 if (entryId.get() == -1) {
399 final var newId = messageSendLogStore.insertIfPossible(message.getTimestamp(),
400 sendResult,
401 contentHint,
402 urgent);
403 entryId.set(newId);
404 } else {
405 messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
406 }
407 }
408 });
409 final var results = sendGroupMessageInternal(legacySender, senderKeySender, recipientIds, distributionId);
410
411 for (var r : results) {
412 handleSendMessageResult(r);
413 }
414
415 return results;
416 }
417
418 private List<SendMessageResult> sendGroupTypingMessage(
419 final SignalServiceTypingMessage message,
420 final Set<RecipientId> recipientIds,
421 final DistributionId distributionId
422 ) throws IOException {
423 final var messageSender = dependencies.getMessageSender();
424 final var results = sendGroupMessageInternal((recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendTyping(
425 recipients,
426 unidentifiedAccess,
427 message,
428 () -> false),
429 (distId, recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendGroupTyping(distId,
430 recipients,
431 unidentifiedAccess,
432 message),
433 recipientIds,
434 distributionId);
435
436 for (var r : results) {
437 handleSendMessageResult(r);
438 }
439
440 return results;
441 }
442
443 private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
444 var g = context.getGroupHelper().getGroup(groupId);
445 if (g == null) {
446 throw new GroupNotFoundException(groupId);
447 }
448 if (!g.isMember(account.getSelfRecipientId())) {
449 throw new NotAGroupMemberException(groupId, g.getTitle());
450 }
451 return g;
452 }
453
454 private List<SendMessageResult> sendGroupMessageInternal(
455 final LegacySenderHandler legacySender,
456 final SenderKeySenderHandler senderKeySender,
457 final Set<RecipientId> recipientIds,
458 final DistributionId distributionId
459 ) throws IOException {
460 long startTime = System.currentTimeMillis();
461 // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false.
462 final var isRecipientUpdate = false;
463 Set<RecipientId> senderKeyTargets = distributionId == null
464 ? Set.of()
465 : getSenderKeyCapableRecipientIds(recipientIds);
466 final var allResults = new ArrayList<SendMessageResult>(recipientIds.size());
467
468 if (senderKeyTargets.size() > 0) {
469 final var results = sendGroupMessageInternalWithSenderKey(senderKeySender,
470 senderKeyTargets,
471 distributionId,
472 isRecipientUpdate);
473
474 if (results == null) {
475 senderKeyTargets = Set.of();
476 } else {
477 results.stream().filter(SendMessageResult::isSuccess).forEach(allResults::add);
478 final var failedTargets = results.stream()
479 .filter(r -> !r.isSuccess())
480 .map(r -> context.getRecipientHelper().resolveRecipient(r.getAddress()))
481 .toList();
482 if (failedTargets.size() > 0) {
483 senderKeyTargets = new HashSet<>(senderKeyTargets);
484 failedTargets.forEach(senderKeyTargets::remove);
485 }
486 }
487 }
488
489 final var legacyTargets = new HashSet<>(recipientIds);
490 legacyTargets.removeAll(senderKeyTargets);
491 final boolean onlyTargetIsSelfWithLinkedDevice = recipientIds.isEmpty() && account.isMultiDevice();
492
493 if (legacyTargets.size() > 0 || onlyTargetIsSelfWithLinkedDevice) {
494 if (legacyTargets.size() > 0) {
495 logger.debug("Need to do {} legacy sends.", legacyTargets.size());
496 } else {
497 logger.debug("Need to do a legacy send to send a sync message for a group of only ourselves.");
498 }
499
500 final List<SendMessageResult> results = sendGroupMessageInternalWithLegacy(legacySender,
501 legacyTargets,
502 isRecipientUpdate || allResults.size() > 0);
503 allResults.addAll(results);
504 }
505 final var duration = Duration.ofMillis(System.currentTimeMillis() - startTime);
506 logger.debug("Sending took {}", duration.toString());
507 return allResults;
508 }
509
510 private Set<RecipientId> getSenderKeyCapableRecipientIds(final Set<RecipientId> recipientIds) {
511 final var selfProfile = context.getProfileHelper().getSelfProfile();
512 if (selfProfile == null || !selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) {
513 logger.debug("Not all of our devices support sender key. Using legacy.");
514 return Set.of();
515 }
516
517 final var senderKeyTargets = new HashSet<RecipientId>();
518 final var recipientList = new ArrayList<>(recipientIds);
519 final var profiles = context.getProfileHelper().getRecipientProfiles(recipientList).iterator();
520 for (final var recipientId : recipientList) {
521 final var profile = profiles.next();
522 if (profile == null || !profile.getCapabilities().contains(Profile.Capability.senderKey)) {
523 continue;
524 }
525
526 final var access = context.getUnidentifiedAccessHelper().getAccessFor(recipientId);
527 if (access.isEmpty() || access.get().getTargetUnidentifiedAccess().isEmpty()) {
528 continue;
529 }
530
531 final var serviceId = account.getRecipientAddressResolver()
532 .resolveRecipientAddress(recipientId)
533 .serviceId()
534 .orElse(null);
535 if (serviceId == null) {
536 continue;
537 }
538 final var identity = account.getIdentityKeyStore().getIdentityInfo(serviceId);
539 if (identity == null || !identity.getTrustLevel().isTrusted()) {
540 continue;
541 }
542
543 senderKeyTargets.add(recipientId);
544 }
545
546 if (senderKeyTargets.size() < 2) {
547 logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
548 return Set.of();
549 }
550
551 logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size());
552 return senderKeyTargets;
553 }
554
555 private List<SendMessageResult> sendGroupMessageInternalWithLegacy(
556 final LegacySenderHandler sender, final Set<RecipientId> recipientIds, final boolean isRecipientUpdate
557 ) throws IOException {
558 final var recipientIdList = new ArrayList<>(recipientIds);
559 final var addresses = recipientIdList.stream()
560 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
561 .toList();
562 final var unidentifiedAccesses = context.getUnidentifiedAccessHelper().getAccessFor(recipientIdList);
563 try {
564 final var results = sender.send(addresses, unidentifiedAccesses, isRecipientUpdate);
565
566 final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
567 logger.debug("Successfully sent using 1:1 to {}/{} legacy targets.", successCount, recipientIdList.size());
568 return results;
569 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
570 return List.of();
571 }
572 }
573
574 private List<SendMessageResult> sendGroupMessageInternalWithSenderKey(
575 final SenderKeySenderHandler sender,
576 final Set<RecipientId> recipientIds,
577 final DistributionId distributionId,
578 final boolean isRecipientUpdate
579 ) throws IOException {
580 final var recipientIdList = new ArrayList<>(recipientIds);
581
582 long keyCreateTime = account.getSenderKeyStore()
583 .getCreateTimeForOurKey(account.getAci(), account.getDeviceId(), distributionId);
584 long keyAge = System.currentTimeMillis() - keyCreateTime;
585
586 if (keyCreateTime != -1 && keyAge > TimeUnit.DAYS.toMillis(14)) {
587 logger.debug("DistributionId {} was created at {} and is {} ms old (~{} days). Rotating.",
588 distributionId,
589 keyCreateTime,
590 keyAge,
591 TimeUnit.MILLISECONDS.toDays(keyAge));
592 account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
593 }
594
595 List<SignalServiceAddress> addresses = recipientIdList.stream()
596 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
597 .toList();
598 List<UnidentifiedAccess> unidentifiedAccesses = context.getUnidentifiedAccessHelper()
599 .getAccessFor(recipientIdList)
600 .stream()
601 .map(Optional::get)
602 .map(UnidentifiedAccessPair::getTargetUnidentifiedAccess)
603 .map(Optional::get)
604 .toList();
605
606 try {
607 List<SendMessageResult> results = sender.send(distributionId,
608 addresses,
609 unidentifiedAccesses,
610 isRecipientUpdate);
611
612 final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
613 logger.debug("Successfully sent using sender key to {}/{} sender key targets.",
614 successCount,
615 addresses.size());
616
617 return results;
618 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
619 return null;
620 } catch (InvalidUnidentifiedAccessHeaderException e) {
621 logger.warn("Someone had a bad UD header. Falling back to legacy sends.", e);
622 return null;
623 } catch (NoSessionException e) {
624 logger.warn("No session. Falling back to legacy sends.", e);
625 account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
626 return null;
627 } catch (InvalidKeyException e) {
628 logger.warn("Invalid key. Falling back to legacy sends.", e);
629 account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
630 return null;
631 } catch (InvalidRegistrationIdException e) {
632 logger.warn("Invalid registrationId. Falling back to legacy sends.", e);
633 return null;
634 } catch (NotFoundException e) {
635 logger.warn("Someone was unregistered. Falling back to legacy sends.", e);
636 return null;
637 } catch (IOException e) {
638 if (e.getCause() instanceof InvalidKeyException) {
639 logger.warn("Invalid key. Falling back to legacy sends.", e);
640 return null;
641 } else {
642 throw e;
643 }
644 }
645 }
646
647 private SendMessageResult sendMessage(
648 SignalServiceDataMessage message, RecipientId recipientId, Optional<Long> editTargetTimestamp
649 ) {
650 final var messageSendLogStore = account.getMessageSendLogStore();
651 final var urgent = true;
652 final var includePniSignature = false;
653 final var result = handleSendMessage(recipientId,
654 editTargetTimestamp.isEmpty()
655 ? (messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address,
656 unidentifiedAccess,
657 ContentHint.RESENDABLE,
658 message,
659 SignalServiceMessageSender.IndividualSendEvents.EMPTY,
660 urgent,
661 includePniSignature)
662 : (messageSender, address, unidentifiedAccess) -> messageSender.sendEditMessage(address,
663 unidentifiedAccess,
664 ContentHint.RESENDABLE,
665 message,
666 SignalServiceMessageSender.IndividualSendEvents.EMPTY,
667 urgent,
668 editTargetTimestamp.get()));
669 messageSendLogStore.insertIfPossible(message.getTimestamp(), result, ContentHint.RESENDABLE, urgent);
670 handleSendMessageResult(result);
671 return result;
672 }
673
674 private SendMessageResult handleSendMessage(RecipientId recipientId, SenderHandler s) {
675 var messageSender = dependencies.getMessageSender();
676
677 var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
678 try {
679 try {
680 return s.send(messageSender, address, context.getUnidentifiedAccessHelper().getAccessFor(recipientId));
681 } catch (UnregisteredUserException e) {
682 final RecipientId newRecipientId;
683 try {
684 newRecipientId = context.getRecipientHelper().refreshRegisteredUser(recipientId);
685 } catch (UnregisteredRecipientException ex) {
686 return SendMessageResult.unregisteredFailure(address);
687 }
688 address = context.getRecipientHelper().resolveSignalServiceAddress(newRecipientId);
689 return s.send(messageSender,
690 address,
691 context.getUnidentifiedAccessHelper().getAccessFor(newRecipientId));
692 }
693 } catch (UnregisteredUserException e) {
694 return SendMessageResult.unregisteredFailure(address);
695 } catch (ProofRequiredException e) {
696 return SendMessageResult.proofRequiredFailure(address, e);
697 } catch (RateLimitException e) {
698 logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage());
699 return SendMessageResult.rateLimitFailure(address, e);
700 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
701 return SendMessageResult.identityFailure(address, e.getIdentityKey());
702 } catch (IOException e) {
703 logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
704 logger.debug("Exception", e);
705 return SendMessageResult.networkFailure(address);
706 }
707 }
708
709 private SendMessageResult sendSelfMessage(SignalServiceDataMessage message, Optional<Long> editTargetTimestamp) {
710 var address = account.getSelfAddress();
711 var transcript = new SentTranscriptMessage(Optional.of(address),
712 message.getTimestamp(),
713 editTargetTimestamp.isEmpty() ? Optional.of(message) : Optional.empty(),
714 message.getExpiresInSeconds(),
715 Map.of(address.getServiceId(), true),
716 false,
717 Optional.empty(),
718 Set.of(),
719 editTargetTimestamp.map((timestamp) -> new SignalServiceEditMessage(timestamp, message)));
720 var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
721
722 return sendSyncMessage(syncMessage);
723 }
724
725 private void handleSendMessageResult(final SendMessageResult r) {
726 if (r.isSuccess() && !r.getSuccess().isUnidentified()) {
727 final var recipientId = context.getRecipientHelper().resolveRecipient(r.getAddress());
728 final var profile = account.getProfileStore().getProfile(recipientId);
729 if (profile != null && (
730 profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.ENABLED
731 || profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED
732 )) {
733 account.getProfileStore()
734 .storeProfile(recipientId,
735 Profile.newBuilder(profile)
736 .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN)
737 .build());
738 }
739 }
740 if (r.isUnregisteredFailure()) {
741 final var recipientId = context.getRecipientHelper().resolveRecipient(r.getAddress());
742 final var profile = account.getProfileStore().getProfile(recipientId);
743 if (profile != null && (
744 profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.ENABLED
745 || profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED
746 )) {
747 account.getProfileStore()
748 .storeProfile(recipientId,
749 Profile.newBuilder(profile)
750 .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN)
751 .build());
752 }
753 }
754 if (r.getIdentityFailure() != null) {
755 final var recipientId = context.getRecipientHelper().resolveRecipient(r.getAddress());
756 context.getIdentityHelper()
757 .handleIdentityFailure(recipientId, r.getAddress().getServiceId(), r.getIdentityFailure());
758 }
759 }
760
761 interface SenderHandler {
762
763 SendMessageResult send(
764 SignalServiceMessageSender messageSender,
765 SignalServiceAddress address,
766 Optional<UnidentifiedAccessPair> unidentifiedAccess
767 ) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
768 }
769
770 interface SenderKeySenderHandler {
771
772 List<SendMessageResult> send(
773 DistributionId distributionId,
774 List<SignalServiceAddress> recipients,
775 List<UnidentifiedAccess> unidentifiedAccess,
776 boolean isRecipientUpdate
777 ) throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException;
778 }
779
780 interface LegacySenderHandler {
781
782 List<SendMessageResult> send(
783 List<SignalServiceAddress> recipients,
784 List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
785 boolean isRecipientUpdate
786 ) throws IOException, UntrustedIdentityException;
787 }
788 }