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