]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java
Improve warning message
[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 selfProfile = context.getProfileHelper().getSelfProfile();
527 if (selfProfile == null || !selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) {
528 logger.debug("Not all of our devices support sender key. Using legacy.");
529 return Set.of();
530 }
531
532 final var senderKeyTargets = new HashSet<RecipientId>();
533 final var recipientList = new ArrayList<>(recipientIds);
534 final var profiles = context.getProfileHelper().getRecipientProfiles(recipientList).iterator();
535 for (final var recipientId : recipientList) {
536 final var profile = profiles.next();
537 if (profile == null || !profile.getCapabilities().contains(Profile.Capability.senderKey)) {
538 continue;
539 }
540
541 final var access = context.getUnidentifiedAccessHelper().getAccessFor(recipientId);
542 if (access.isEmpty() || access.get().getTargetUnidentifiedAccess().isEmpty()) {
543 continue;
544 }
545
546 final var serviceId = account.getRecipientAddressResolver()
547 .resolveRecipientAddress(recipientId)
548 .serviceId()
549 .orElse(null);
550 if (serviceId == null) {
551 continue;
552 }
553 final var identity = account.getIdentityKeyStore().getIdentityInfo(serviceId);
554 if (identity == null || !identity.getTrustLevel().isTrusted()) {
555 continue;
556 }
557
558 senderKeyTargets.add(recipientId);
559 }
560
561 if (senderKeyTargets.size() < 2) {
562 logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
563 return Set.of();
564 }
565
566 logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size());
567 return senderKeyTargets;
568 }
569
570 private List<SendMessageResult> sendGroupMessageInternalWithLegacy(
571 final LegacySenderHandler sender, final Set<RecipientId> recipientIds, final boolean isRecipientUpdate
572 ) throws IOException {
573 final var recipientIdList = new ArrayList<>(recipientIds);
574 final var addresses = recipientIdList.stream()
575 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
576 .toList();
577 final var unidentifiedAccesses = context.getUnidentifiedAccessHelper().getAccessFor(recipientIdList);
578 try {
579 final var results = sender.send(addresses, unidentifiedAccesses, isRecipientUpdate);
580
581 final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
582 logger.debug("Successfully sent using 1:1 to {}/{} legacy targets.", successCount, recipientIdList.size());
583 return results;
584 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
585 return List.of();
586 }
587 }
588
589 private List<SendMessageResult> sendGroupMessageInternalWithSenderKey(
590 final SenderKeySenderHandler sender,
591 final Set<RecipientId> recipientIds,
592 final DistributionId distributionId,
593 final boolean isRecipientUpdate
594 ) throws IOException {
595 final var recipientIdList = new ArrayList<>(recipientIds);
596
597 long keyCreateTime = account.getSenderKeyStore()
598 .getCreateTimeForOurKey(account.getAci(), account.getDeviceId(), distributionId);
599 long keyAge = System.currentTimeMillis() - keyCreateTime;
600
601 if (keyCreateTime != -1 && keyAge > TimeUnit.DAYS.toMillis(14)) {
602 logger.debug("DistributionId {} was created at {} and is {} ms old (~{} days). Rotating.",
603 distributionId,
604 keyCreateTime,
605 keyAge,
606 TimeUnit.MILLISECONDS.toDays(keyAge));
607 account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
608 }
609
610 List<SignalServiceAddress> addresses = recipientIdList.stream()
611 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
612 .toList();
613 List<UnidentifiedAccess> unidentifiedAccesses = context.getUnidentifiedAccessHelper()
614 .getAccessFor(recipientIdList)
615 .stream()
616 .map(Optional::get)
617 .map(UnidentifiedAccessPair::getTargetUnidentifiedAccess)
618 .map(Optional::get)
619 .toList();
620
621 try {
622 List<SendMessageResult> results = sender.send(distributionId,
623 addresses,
624 unidentifiedAccesses,
625 isRecipientUpdate);
626
627 final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
628 logger.debug("Successfully sent using sender key to {}/{} sender key targets.",
629 successCount,
630 addresses.size());
631
632 return results;
633 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
634 return null;
635 } catch (InvalidUnidentifiedAccessHeaderException e) {
636 logger.warn("Someone had a bad UD header. Falling back to legacy sends.", e);
637 return null;
638 } catch (NoSessionException e) {
639 logger.warn("No session. Falling back to legacy sends.", e);
640 account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
641 return null;
642 } catch (InvalidKeyException e) {
643 logger.warn("Invalid key. Falling back to legacy sends.", e);
644 account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
645 return null;
646 } catch (InvalidRegistrationIdException e) {
647 logger.warn("Invalid registrationId. Falling back to legacy sends.", e);
648 return null;
649 } catch (NotFoundException e) {
650 logger.warn("Someone was unregistered. Falling back to legacy sends.", e);
651 return null;
652 } catch (IOException e) {
653 if (e.getCause() instanceof InvalidKeyException) {
654 logger.warn("Invalid key. Falling back to legacy sends.", e);
655 return null;
656 } else {
657 throw e;
658 }
659 }
660 }
661
662 private SendMessageResult sendMessage(
663 SignalServiceDataMessage message, RecipientId recipientId, Optional<Long> editTargetTimestamp
664 ) {
665 final var messageSendLogStore = account.getMessageSendLogStore();
666 final var urgent = true;
667 final var result = handleSendMessage(recipientId,
668 editTargetTimestamp.isEmpty()
669 ? (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendDataMessage(
670 address,
671 unidentifiedAccess,
672 ContentHint.RESENDABLE,
673 message,
674 SignalServiceMessageSender.IndividualSendEvents.EMPTY,
675 urgent,
676 includePniSignature)
677 : (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendEditMessage(
678 address,
679 unidentifiedAccess,
680 ContentHint.RESENDABLE,
681 message,
682 SignalServiceMessageSender.IndividualSendEvents.EMPTY,
683 urgent,
684 editTargetTimestamp.get()));
685 messageSendLogStore.insertIfPossible(message.getTimestamp(), result, ContentHint.RESENDABLE, urgent);
686 handleSendMessageResult(result);
687 return result;
688 }
689
690 private SendMessageResult handleSendMessage(RecipientId recipientId, SenderHandler s) {
691 var messageSender = dependencies.getMessageSender();
692
693 var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
694 try {
695 final boolean includePniSignature = account.getRecipientStore().needsPniSignature(recipientId);
696 try {
697 return s.send(messageSender,
698 address,
699 context.getUnidentifiedAccessHelper().getAccessFor(recipientId),
700 includePniSignature);
701 } catch (UnregisteredUserException e) {
702 final RecipientId newRecipientId;
703 try {
704 newRecipientId = context.getRecipientHelper().refreshRegisteredUser(recipientId);
705 } catch (UnregisteredRecipientException ex) {
706 return SendMessageResult.unregisteredFailure(address);
707 }
708 address = context.getRecipientHelper().resolveSignalServiceAddress(newRecipientId);
709 return s.send(messageSender,
710 address,
711 context.getUnidentifiedAccessHelper().getAccessFor(newRecipientId),
712 includePniSignature);
713 }
714 } catch (UnregisteredUserException e) {
715 return SendMessageResult.unregisteredFailure(address);
716 } catch (ProofRequiredException e) {
717 return SendMessageResult.proofRequiredFailure(address, e);
718 } catch (RateLimitException e) {
719 logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage());
720 return SendMessageResult.rateLimitFailure(address, e);
721 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
722 return SendMessageResult.identityFailure(address, e.getIdentityKey());
723 } catch (IOException e) {
724 logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
725 logger.debug("Exception", e);
726 return SendMessageResult.networkFailure(address);
727 }
728 }
729
730 private SendMessageResult sendSelfMessage(SignalServiceDataMessage message, Optional<Long> editTargetTimestamp) {
731 var address = account.getSelfAddress();
732 var transcript = new SentTranscriptMessage(Optional.of(address),
733 message.getTimestamp(),
734 editTargetTimestamp.isEmpty() ? Optional.of(message) : Optional.empty(),
735 message.getExpiresInSeconds(),
736 Map.of(address.getServiceId(), true),
737 false,
738 Optional.empty(),
739 Set.of(),
740 editTargetTimestamp.map((timestamp) -> new SignalServiceEditMessage(timestamp, message)));
741 var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
742
743 return sendSyncMessage(syncMessage);
744 }
745
746 private void handleSendMessageResult(final SendMessageResult r) {
747 if (r.isSuccess() && !r.getSuccess().isUnidentified()) {
748 final var recipientId = account.getRecipientResolver().resolveRecipient(r.getAddress());
749 final var profile = account.getProfileStore().getProfile(recipientId);
750 if (profile != null && (
751 profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.ENABLED
752 || profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED
753 )) {
754 account.getProfileStore()
755 .storeProfile(recipientId,
756 Profile.newBuilder(profile)
757 .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN)
758 .build());
759 }
760 }
761 if (r.isUnregisteredFailure()) {
762 final var recipientId = account.getRecipientResolver().resolveRecipient(r.getAddress());
763 final var profile = account.getProfileStore().getProfile(recipientId);
764 if (profile != null && (
765 profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.ENABLED
766 || profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED
767 )) {
768 account.getProfileStore()
769 .storeProfile(recipientId,
770 Profile.newBuilder(profile)
771 .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN)
772 .build());
773 }
774 }
775 if (r.getIdentityFailure() != null) {
776 final var recipientId = account.getRecipientResolver().resolveRecipient(r.getAddress());
777 context.getIdentityHelper()
778 .handleIdentityFailure(recipientId, r.getAddress().getServiceId(), r.getIdentityFailure());
779 }
780 }
781
782 interface SenderHandler {
783
784 SendMessageResult send(
785 SignalServiceMessageSender messageSender,
786 SignalServiceAddress address,
787 Optional<UnidentifiedAccessPair> unidentifiedAccess,
788 boolean includePniSignature
789 ) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
790 }
791
792 interface SenderKeySenderHandler {
793
794 List<SendMessageResult> send(
795 DistributionId distributionId,
796 List<SignalServiceAddress> recipients,
797 List<UnidentifiedAccess> unidentifiedAccess,
798 boolean isRecipientUpdate
799 ) throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException;
800 }
801
802 interface LegacySenderHandler {
803
804 List<SendMessageResult> send(
805 List<SignalServiceAddress> recipients,
806 List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
807 boolean isRecipientUpdate
808 ) throws IOException, UntrustedIdentityException;
809 }
810 }