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