]> 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 final var expirationTime = contact.messageExpirationTime();
90 messageBuilder.withExpiration(expirationTime);
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, final RecipientId recipientId
129 ) {
130 final var messageSendLogStore = account.getMessageSendLogStore();
131 final var result = handleSendMessage(recipientId,
132 (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendReceipt(address,
133 unidentifiedAccess,
134 receiptMessage,
135 includePniSignature));
136 messageSendLogStore.insertIfPossible(receiptMessage.getWhen(), result, ContentHint.IMPLICIT, false);
137 handleSendMessageResult(result);
138 return result;
139 }
140
141 public SendMessageResult sendProfileKey(RecipientId recipientId) {
142 logger.debug("Sending updated profile key to recipient: {}", recipientId);
143 final var profileKey = account.getProfileKey().serialize();
144 final var message = SignalServiceDataMessage.newBuilder()
145 .asProfileKeyUpdate(true)
146 .withProfileKey(profileKey)
147 .build();
148 return handleSendMessage(recipientId,
149 (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendDataMessage(
150 address,
151 unidentifiedAccess,
152 ContentHint.IMPLICIT,
153 message,
154 SignalServiceMessageSender.IndividualSendEvents.EMPTY,
155 false,
156 includePniSignature));
157 }
158
159 public SendMessageResult sendRetryReceipt(
160 DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional<GroupId> groupId
161 ) {
162 logger.debug("Sending retry receipt for {} to {}, device: {}",
163 errorMessage.getTimestamp(),
164 recipientId,
165 errorMessage.getDeviceId());
166 final var result = handleSendMessage(recipientId,
167 (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendRetryReceipt(
168 address,
169 unidentifiedAccess,
170 groupId.map(GroupId::serialize),
171 errorMessage));
172 handleSendMessageResult(result);
173 return result;
174 }
175
176 public SendMessageResult sendNullMessage(RecipientId recipientId) {
177 final var result = handleSendMessage(recipientId,
178 (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendNullMessage(
179 address,
180 unidentifiedAccess));
181 handleSendMessageResult(result);
182 return result;
183 }
184
185 public SendMessageResult sendSelfMessage(
186 SignalServiceDataMessage.Builder messageBuilder, Optional<Long> editTargetTimestamp
187 ) {
188 final var recipientId = account.getSelfRecipientId();
189 final var contact = account.getContactStore().getContact(recipientId);
190 final var expirationTime = contact != null ? contact.messageExpirationTime() : 0;
191 messageBuilder.withExpiration(expirationTime);
192
193 var message = messageBuilder.build();
194 return sendSelfMessage(message, editTargetTimestamp);
195 }
196
197 public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message) {
198 var messageSender = dependencies.getMessageSender();
199 if (!account.isMultiDevice()) {
200 logger.trace("Not sending sync message because there are no linked devices.");
201 return SendMessageResult.success(account.getSelfAddress(), List.of(), false, false, 0, Optional.empty());
202 }
203 try {
204 return messageSender.sendSyncMessage(message);
205 } catch (Throwable e) {
206 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
207 try {
208 return SignalServiceMessageSender.mapSendErrorToSendResult(e, System.currentTimeMillis(), address);
209 } catch (IOException ex) {
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
217 public SendMessageResult sendTypingMessage(
218 SignalServiceTypingMessage message, RecipientId recipientId
219 ) {
220 final var result = handleSendMessage(recipientId,
221 (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendTyping(List.of(
222 address), List.of(unidentifiedAccess), message, null).getFirst());
223 handleSendMessageResult(result);
224 return result;
225 }
226
227 public List<SendMessageResult> sendGroupTypingMessage(
228 SignalServiceTypingMessage message, GroupId groupId
229 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
230 final var g = getGroupForSending(groupId);
231 if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
232 throw new GroupSendingNotAllowedException(groupId, g.getTitle());
233 }
234 final var distributionId = g.getDistributionId();
235 final var recipientIds = g.getMembersWithout(account.getSelfRecipientId());
236
237 return sendGroupTypingMessage(message, recipientIds, distributionId);
238 }
239
240 public SendMessageResult resendMessage(
241 final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry
242 ) {
243 logger.trace("Resending message {} to {}", timestamp, recipientId);
244 if (messageSendLogEntry.groupId().isEmpty()) {
245 return handleSendMessage(recipientId,
246 (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.resendContent(
247 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 = context.getGroupHelper().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.of(senderKeyDistributionMessage.serialize());
271 final var contentToSend = messageSendLogEntry.content()
272 .newBuilder()
273 .senderKeyDistributionMessage(distributionBytes)
274 .build();
275
276 final var result = handleSendMessage(recipientId,
277 (messageSender, address, unidentifiedAccess, includePniSignature) -> 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,
301 final GroupInfo g,
302 final boolean includeSelf,
303 final Optional<Long> editTargetTimestamp
304 ) throws IOException, GroupSendingNotAllowedException {
305 GroupUtils.setGroupContext(messageBuilder, g);
306 messageBuilder.withExpiration(g.getMessageExpirationTimer());
307
308 final var message = messageBuilder.build();
309 final var recipients = includeSelf ? g.getMembers() : g.getMembersWithout(account.getSelfRecipientId());
310
311 if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
312 if (message.getBody().isPresent()
313 || message.getAttachments().isPresent()
314 || message.getQuote().isPresent()
315 || message.getPreviews().isPresent()
316 || message.getMentions().isPresent()
317 || message.getSticker().isPresent()) {
318 throw new GroupSendingNotAllowedException(g.getGroupId(), g.getTitle());
319 }
320 }
321
322 return sendGroupMessage(message,
323 recipients,
324 g.getDistributionId(),
325 ContentHint.RESENDABLE,
326 editTargetTimestamp);
327 }
328
329 private List<SendMessageResult> sendGroupMessage(
330 final SignalServiceDataMessage message,
331 final Set<RecipientId> recipientIds,
332 final DistributionId distributionId,
333 final ContentHint contentHint,
334 final Optional<Long> editTargetTimestamp
335 ) throws IOException {
336 final var messageSender = dependencies.getMessageSender();
337 final var messageSendLogStore = account.getMessageSendLogStore();
338 final AtomicLong entryId = new AtomicLong(-1);
339
340 final var urgent = true;
341 final PartialSendCompleteListener partialSendCompleteListener = sendResult -> {
342 logger.trace("Partial message send result: {}", sendResult.isSuccess());
343 synchronized (entryId) {
344 if (entryId.get() == -1) {
345 final var newId = messageSendLogStore.insertIfPossible(message.getTimestamp(),
346 sendResult,
347 contentHint,
348 urgent);
349 entryId.set(newId);
350 } else {
351 messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
352 }
353 }
354 };
355 final LegacySenderHandler legacySender = (recipients, unidentifiedAccess, isRecipientUpdate) ->
356 editTargetTimestamp.isEmpty()
357 ? messageSender.sendDataMessage(recipients,
358 unidentifiedAccess,
359 isRecipientUpdate,
360 contentHint,
361 message,
362 SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
363 partialSendCompleteListener,
364 () -> false,
365 urgent)
366 : messageSender.sendEditMessage(recipients,
367 unidentifiedAccess,
368 isRecipientUpdate,
369 contentHint,
370 message,
371 SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
372 partialSendCompleteListener,
373 () -> false,
374 urgent,
375 editTargetTimestamp.get());
376 final SenderKeySenderHandler senderKeySender = (distId, recipients, unidentifiedAccess, groupSendEndorsements, isRecipientUpdate) -> messageSender.sendGroupDataMessage(
377 distId,
378 recipients,
379 unidentifiedAccess,
380 groupSendEndorsements,
381 isRecipientUpdate,
382 contentHint,
383 message,
384 SignalServiceMessageSender.SenderKeyGroupEvents.EMPTY,
385 urgent,
386 false,
387 editTargetTimestamp.map(timestamp -> new SignalServiceEditMessage(timestamp, message)).orElse(null),
388 sendResult -> {
389 logger.trace("Partial message send results: {}", sendResult.size());
390 synchronized (entryId) {
391 if (entryId.get() == -1) {
392 final var newId = messageSendLogStore.insertIfPossible(message.getTimestamp(),
393 sendResult,
394 contentHint,
395 urgent);
396 entryId.set(newId);
397 } else {
398 messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
399 }
400 }
401 synchronized (entryId) {
402 if (entryId.get() == -1) {
403 final var newId = messageSendLogStore.insertIfPossible(message.getTimestamp(),
404 sendResult,
405 contentHint,
406 urgent);
407 entryId.set(newId);
408 } else {
409 messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
410 }
411 }
412 });
413 final var results = sendGroupMessageInternal(legacySender, senderKeySender, recipientIds, distributionId);
414
415 for (var r : results) {
416 handleSendMessageResult(r);
417 }
418
419 return results;
420 }
421
422 private List<SendMessageResult> sendGroupTypingMessage(
423 final SignalServiceTypingMessage message,
424 final Set<RecipientId> recipientIds,
425 final DistributionId distributionId
426 ) throws IOException {
427 final var messageSender = dependencies.getMessageSender();
428 final var results = sendGroupMessageInternal((recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendTyping(
429 recipients,
430 unidentifiedAccess,
431 message,
432 () -> false),
433 (distId, recipients, unidentifiedAccess, groupSendEndorsements, isRecipientUpdate) -> messageSender.sendGroupTyping(
434 distId,
435 recipients,
436 unidentifiedAccess,
437 groupSendEndorsements,
438 message),
439 recipientIds,
440 distributionId);
441
442 for (var r : results) {
443 handleSendMessageResult(r);
444 }
445
446 return results;
447 }
448
449 private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
450 var g = context.getGroupHelper().getGroup(groupId);
451 if (g == null) {
452 throw new GroupNotFoundException(groupId);
453 }
454 if (!g.isMember(account.getSelfRecipientId())) {
455 throw new NotAGroupMemberException(groupId, g.getTitle());
456 }
457 if (!g.isProfileSharingEnabled()) {
458 g.setProfileSharingEnabled(true);
459 account.getGroupStore().updateGroup(g);
460 }
461 return g;
462 }
463
464 private List<SendMessageResult> sendGroupMessageInternal(
465 final LegacySenderHandler legacySender,
466 final SenderKeySenderHandler senderKeySender,
467 final Set<RecipientId> recipientIds,
468 final DistributionId distributionId
469 ) throws IOException {
470 long startTime = System.currentTimeMillis();
471 // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false.
472 final var isRecipientUpdate = false;
473 Set<RecipientId> senderKeyTargets = distributionId == null
474 ? Set.of()
475 : getSenderKeyCapableRecipientIds(recipientIds);
476 final var allResults = new ArrayList<SendMessageResult>(recipientIds.size());
477
478 if (!senderKeyTargets.isEmpty()) {
479 final var results = sendGroupMessageInternalWithSenderKey(senderKeySender,
480 senderKeyTargets,
481 distributionId,
482 isRecipientUpdate);
483
484 if (results == null) {
485 senderKeyTargets = Set.of();
486 } else {
487 results.stream().filter(SendMessageResult::isSuccess).forEach(allResults::add);
488 final var recipientResolver = account.getRecipientResolver();
489 final var failedTargets = results.stream()
490 .filter(r -> !r.isSuccess())
491 .map(r -> recipientResolver.resolveRecipient(r.getAddress()))
492 .toList();
493 if (!failedTargets.isEmpty()) {
494 senderKeyTargets = new HashSet<>(senderKeyTargets);
495 failedTargets.forEach(senderKeyTargets::remove);
496 }
497 }
498 }
499
500 final var legacyTargets = new HashSet<>(recipientIds);
501 legacyTargets.removeAll(senderKeyTargets);
502 final boolean onlyTargetIsSelfWithLinkedDevice = recipientIds.isEmpty() && account.isMultiDevice();
503
504 if (!legacyTargets.isEmpty() || onlyTargetIsSelfWithLinkedDevice) {
505 if (!legacyTargets.isEmpty()) {
506 logger.debug("Need to do {} legacy sends.", legacyTargets.size());
507 } else {
508 logger.debug("Need to do a legacy send to send a sync message for a group of only ourselves.");
509 }
510
511 final List<SendMessageResult> results = sendGroupMessageInternalWithLegacy(legacySender,
512 legacyTargets,
513 isRecipientUpdate || !allResults.isEmpty());
514 allResults.addAll(results);
515 }
516 final var duration = Duration.ofMillis(System.currentTimeMillis() - startTime);
517 logger.debug("Sending took {}", duration.toString());
518 return allResults;
519 }
520
521 private Set<RecipientId> getSenderKeyCapableRecipientIds(final Set<RecipientId> recipientIds) {
522 final var senderKeyTargets = new HashSet<RecipientId>();
523 final var recipientList = new ArrayList<>(recipientIds);
524 for (final var recipientId : recipientList) {
525 final var access = context.getUnidentifiedAccessHelper().getSealedSenderAccessFor(recipientId);
526 if (access == null) {
527 continue;
528 }
529
530 final var serviceId = account.getRecipientAddressResolver()
531 .resolveRecipientAddress(recipientId)
532 .serviceId()
533 .orElse(null);
534 if (serviceId == null) {
535 continue;
536 }
537 final var identity = account.getIdentityKeyStore().getIdentityInfo(serviceId);
538 if (identity == null || !identity.getTrustLevel().isTrusted()) {
539 continue;
540 }
541
542 senderKeyTargets.add(recipientId);
543 }
544
545 if (senderKeyTargets.size() < 2) {
546 logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
547 return Set.of();
548 }
549
550 logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size());
551 return senderKeyTargets;
552 }
553
554 private List<SendMessageResult> sendGroupMessageInternalWithLegacy(
555 final LegacySenderHandler sender, final Set<RecipientId> recipientIds, final boolean isRecipientUpdate
556 ) throws IOException {
557 final var recipientIdList = new ArrayList<>(recipientIds);
558 final var addresses = recipientIdList.stream()
559 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
560 .toList();
561 final var unidentifiedAccesses = context.getUnidentifiedAccessHelper()
562 .getSealedSenderAccessFor(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 .toList();
602
603 final GroupSendEndorsements groupSendEndorsements = null;//TODO
604 try {
605 List<SendMessageResult> results = sender.send(distributionId,
606 addresses,
607 unidentifiedAccesses,
608 groupSendEndorsements,
609 isRecipientUpdate);
610
611 final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
612 logger.debug("Successfully sent using sender key to {}/{} sender key targets.",
613 successCount,
614 addresses.size());
615
616 return results;
617 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
618 return null;
619 } catch (InvalidUnidentifiedAccessHeaderException e) {
620 logger.warn("Someone had a bad UD header. Falling back to legacy sends.", e);
621 return null;
622 } catch (NoSessionException e) {
623 logger.warn("No session. Falling back to legacy sends.", e);
624 account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
625 return null;
626 } catch (InvalidKeyException e) {
627 logger.warn("Invalid key. Falling back to legacy sends.", e);
628 account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
629 return null;
630 } catch (InvalidRegistrationIdException e) {
631 logger.warn("Invalid registrationId. Falling back to legacy sends.", e);
632 return null;
633 } catch (NotFoundException e) {
634 logger.warn("Someone was unregistered. Falling back to legacy sends.", e);
635 return null;
636 } catch (IOException e) {
637 if (e.getCause() instanceof InvalidKeyException) {
638 logger.warn("Invalid key. Falling back to legacy sends.", e);
639 return null;
640 } else {
641 throw e;
642 }
643 }
644 }
645
646 private SendMessageResult sendMessage(
647 SignalServiceDataMessage message, RecipientId recipientId, Optional<Long> editTargetTimestamp
648 ) {
649 final var messageSendLogStore = account.getMessageSendLogStore();
650 final var urgent = true;
651 final var result = handleSendMessage(recipientId,
652 editTargetTimestamp.isEmpty()
653 ? (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendDataMessage(
654 address,
655 unidentifiedAccess,
656 ContentHint.RESENDABLE,
657 message,
658 SignalServiceMessageSender.IndividualSendEvents.EMPTY,
659 urgent,
660 includePniSignature)
661 : (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendEditMessage(
662 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 final boolean includePniSignature = account.getRecipientStore().needsPniSignature(recipientId);
680 try {
681 return s.send(messageSender,
682 address,
683 context.getUnidentifiedAccessHelper().getSealedSenderAccessFor(recipientId),
684 includePniSignature);
685 } catch (UnregisteredUserException e) {
686 final RecipientId newRecipientId;
687 try {
688 newRecipientId = context.getRecipientHelper().refreshRegisteredUser(recipientId);
689 } catch (UnregisteredRecipientException ex) {
690 return SendMessageResult.unregisteredFailure(address);
691 }
692 address = context.getRecipientHelper().resolveSignalServiceAddress(newRecipientId);
693 return s.send(messageSender,
694 address,
695 context.getUnidentifiedAccessHelper().getSealedSenderAccessFor(newRecipientId),
696 includePniSignature);
697 }
698 } catch (Throwable e) {
699 try {
700 return SignalServiceMessageSender.mapSendErrorToSendResult(e, System.currentTimeMillis(), address);
701 } catch (IOException ex) {
702 logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
703 logger.debug("Exception", e);
704 return SendMessageResult.networkFailure(address);
705 }
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 = account.getRecipientResolver().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 = account.getRecipientResolver().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 = account.getRecipientResolver().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 @Nullable SealedSenderAccess unidentifiedAccess,
767 boolean includePniSignature
768 ) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
769 }
770
771 interface SenderKeySenderHandler {
772
773 List<SendMessageResult> send(
774 DistributionId distributionId,
775 List<SignalServiceAddress> recipients,
776 List<UnidentifiedAccess> unidentifiedAccess,
777 GroupSendEndorsements groupSendEndorsements,
778 boolean isRecipientUpdate
779 ) throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException;
780 }
781
782 interface LegacySenderHandler {
783
784 List<SendMessageResult> send(
785 List<SignalServiceAddress> recipients,
786 List<SealedSenderAccess> unidentifiedAccess,
787 boolean isRecipientUpdate
788 ) throws IOException, UntrustedIdentityException;
789 }
790 }