]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java
Add --notify-self parmeter
[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) -> messageSender.sendReceipt(address,
131 unidentifiedAccess,
132 receiptMessage,
133 false));
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) -> messageSender.sendDataMessage(address,
148 unidentifiedAccess,
149 ContentHint.IMPLICIT,
150 message,
151 SignalServiceMessageSender.IndividualSendEvents.EMPTY,
152 false,
153 false));
154 }
155
156 public SendMessageResult sendRetryReceipt(
157 DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional<GroupId> groupId
158 ) {
159 logger.debug("Sending retry receipt for {} to {}, device: {}",
160 errorMessage.getTimestamp(),
161 recipientId,
162 errorMessage.getDeviceId());
163 final var result = handleSendMessage(recipientId,
164 (messageSender, address, unidentifiedAccess) -> messageSender.sendRetryReceipt(address,
165 unidentifiedAccess,
166 groupId.map(GroupId::serialize),
167 errorMessage));
168 handleSendMessageResult(result);
169 return result;
170 }
171
172 public SendMessageResult sendNullMessage(RecipientId recipientId) {
173 final var result = handleSendMessage(recipientId, SignalServiceMessageSender::sendNullMessage);
174 handleSendMessageResult(result);
175 return result;
176 }
177
178 public SendMessageResult sendSelfMessage(
179 SignalServiceDataMessage.Builder messageBuilder, Optional<Long> editTargetTimestamp
180 ) {
181 final var recipientId = account.getSelfRecipientId();
182 final var contact = account.getContactStore().getContact(recipientId);
183 final var expirationTime = contact != null ? contact.messageExpirationTime() : 0;
184 messageBuilder.withExpiration(expirationTime);
185
186 var message = messageBuilder.build();
187 return sendSelfMessage(message, editTargetTimestamp);
188 }
189
190 public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message) {
191 var messageSender = dependencies.getMessageSender();
192 if (!account.isMultiDevice()) {
193 logger.trace("Not sending sync message because there are no linked devices.");
194 return SendMessageResult.success(account.getSelfAddress(), List.of(), false, false, 0, Optional.empty());
195 }
196 try {
197 return messageSender.sendSyncMessage(message, context.getUnidentifiedAccessHelper().getAccessForSync());
198 } catch (UnregisteredUserException e) {
199 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
200 return SendMessageResult.unregisteredFailure(address);
201 } catch (ProofRequiredException e) {
202 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
203 return SendMessageResult.proofRequiredFailure(address, e);
204 } catch (RateLimitException e) {
205 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
206 logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage());
207 return SendMessageResult.rateLimitFailure(address, e);
208 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
209 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
210 return SendMessageResult.identityFailure(address, e.getIdentityKey());
211 } catch (IOException e) {
212 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
213 logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
214 logger.debug("Exception", e);
215 return SendMessageResult.networkFailure(address);
216 }
217 }
218
219 public SendMessageResult sendTypingMessage(
220 SignalServiceTypingMessage message, RecipientId recipientId
221 ) {
222 final var result = handleSendMessage(recipientId,
223 (messageSender, address, unidentifiedAccess) -> messageSender.sendTyping(List.of(address),
224 List.of(unidentifiedAccess),
225 message,
226 null).getFirst());
227 handleSendMessageResult(result);
228 return result;
229 }
230
231 public List<SendMessageResult> sendGroupTypingMessage(
232 SignalServiceTypingMessage message, GroupId groupId
233 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
234 final var g = getGroupForSending(groupId);
235 if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
236 throw new GroupSendingNotAllowedException(groupId, g.getTitle());
237 }
238 final var distributionId = g.getDistributionId();
239 final var recipientIds = g.getMembersWithout(account.getSelfRecipientId());
240
241 return sendGroupTypingMessage(message, recipientIds, distributionId);
242 }
243
244 public SendMessageResult resendMessage(
245 final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry
246 ) {
247 logger.trace("Resending message {} to {}", timestamp, recipientId);
248 if (messageSendLogEntry.groupId().isEmpty()) {
249 return handleSendMessage(recipientId,
250 (messageSender, address, unidentifiedAccess) -> messageSender.resendContent(address,
251 unidentifiedAccess,
252 timestamp,
253 messageSendLogEntry.content(),
254 messageSendLogEntry.contentHint(),
255 Optional.empty(),
256 messageSendLogEntry.urgent()));
257 }
258
259 final var groupId = messageSendLogEntry.groupId().get();
260 final var group = account.getGroupStore().getGroup(groupId);
261
262 if (group == null) {
263 logger.debug("Could not find a matching group for the groupId {}! Skipping message send.",
264 groupId.toBase64());
265 return null;
266 } else if (!group.getMembers().contains(recipientId)) {
267 logger.warn("The target user is no longer in the group {}! Skipping message send.", groupId.toBase64());
268 return null;
269 }
270
271 final var senderKeyDistributionMessage = dependencies.getMessageSender()
272 .getOrCreateNewGroupSession(group.getDistributionId());
273 final var distributionBytes = ByteString.of(senderKeyDistributionMessage.serialize());
274 final var contentToSend = messageSendLogEntry.content()
275 .newBuilder()
276 .senderKeyDistributionMessage(distributionBytes)
277 .build();
278
279 final var result = handleSendMessage(recipientId,
280 (messageSender, address, unidentifiedAccess) -> messageSender.resendContent(address,
281 unidentifiedAccess,
282 timestamp,
283 contentToSend,
284 messageSendLogEntry.contentHint(),
285 Optional.of(group.getGroupId().serialize()),
286 messageSendLogEntry.urgent()));
287
288 if (result.isSuccess()) {
289 final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
290 final var addresses = result.getSuccess()
291 .getDevices()
292 .stream()
293 .map(device -> new SignalProtocolAddress(address.getIdentifier(), device))
294 .toList();
295
296 account.getSenderKeyStore().markSenderKeySharedWith(group.getDistributionId(), addresses);
297 }
298
299 return result;
300 }
301
302 private List<SendMessageResult> sendAsGroupMessage(
303 final SignalServiceDataMessage.Builder messageBuilder,
304 final GroupInfo g,
305 final boolean includeSelf,
306 final Optional<Long> editTargetTimestamp
307 ) throws IOException, GroupSendingNotAllowedException {
308 GroupUtils.setGroupContext(messageBuilder, g);
309 messageBuilder.withExpiration(g.getMessageExpirationTimer());
310
311 final var message = messageBuilder.build();
312 final var recipients = includeSelf ? g.getMembers() : g.getMembersWithout(account.getSelfRecipientId());
313
314 if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
315 if (message.getBody().isPresent()
316 || message.getAttachments().isPresent()
317 || message.getQuote().isPresent()
318 || message.getPreviews().isPresent()
319 || message.getMentions().isPresent()
320 || message.getSticker().isPresent()) {
321 throw new GroupSendingNotAllowedException(g.getGroupId(), g.getTitle());
322 }
323 }
324
325 return sendGroupMessage(message,
326 recipients,
327 g.getDistributionId(),
328 ContentHint.RESENDABLE,
329 editTargetTimestamp);
330 }
331
332 private List<SendMessageResult> sendGroupMessage(
333 final SignalServiceDataMessage message,
334 final Set<RecipientId> recipientIds,
335 final DistributionId distributionId,
336 final ContentHint contentHint,
337 final Optional<Long> editTargetTimestamp
338 ) throws IOException {
339 final var messageSender = dependencies.getMessageSender();
340 final var messageSendLogStore = account.getMessageSendLogStore();
341 final AtomicLong entryId = new AtomicLong(-1);
342
343 final var urgent = true;
344 final PartialSendCompleteListener partialSendCompleteListener = sendResult -> {
345 logger.trace("Partial message send result: {}", sendResult.isSuccess());
346 synchronized (entryId) {
347 if (entryId.get() == -1) {
348 final var newId = messageSendLogStore.insertIfPossible(message.getTimestamp(),
349 sendResult,
350 contentHint,
351 urgent);
352 entryId.set(newId);
353 } else {
354 messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
355 }
356 }
357 };
358 final LegacySenderHandler legacySender = (recipients, unidentifiedAccess, isRecipientUpdate) ->
359 editTargetTimestamp.isEmpty()
360 ? messageSender.sendDataMessage(recipients,
361 unidentifiedAccess,
362 isRecipientUpdate,
363 contentHint,
364 message,
365 SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
366 partialSendCompleteListener,
367 () -> false,
368 urgent)
369 : messageSender.sendEditMessage(recipients,
370 unidentifiedAccess,
371 isRecipientUpdate,
372 contentHint,
373 message,
374 SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
375 partialSendCompleteListener,
376 () -> false,
377 urgent,
378 editTargetTimestamp.get());
379 final SenderKeySenderHandler senderKeySender = (distId, recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendGroupDataMessage(
380 distId,
381 recipients,
382 unidentifiedAccess,
383 isRecipientUpdate,
384 contentHint,
385 message,
386 SignalServiceMessageSender.SenderKeyGroupEvents.EMPTY,
387 urgent,
388 false,
389 editTargetTimestamp.map(timestamp -> new SignalServiceEditMessage(timestamp, message)).orElse(null),
390 sendResult -> {
391 logger.trace("Partial message send results: {}", sendResult.size());
392 synchronized (entryId) {
393 if (entryId.get() == -1) {
394 final var newId = messageSendLogStore.insertIfPossible(message.getTimestamp(),
395 sendResult,
396 contentHint,
397 urgent);
398 entryId.set(newId);
399 } else {
400 messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
401 }
402 }
403 synchronized (entryId) {
404 if (entryId.get() == -1) {
405 final var newId = messageSendLogStore.insertIfPossible(message.getTimestamp(),
406 sendResult,
407 contentHint,
408 urgent);
409 entryId.set(newId);
410 } else {
411 messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
412 }
413 }
414 });
415 final var results = sendGroupMessageInternal(legacySender, senderKeySender, recipientIds, distributionId);
416
417 for (var r : results) {
418 handleSendMessageResult(r);
419 }
420
421 return results;
422 }
423
424 private List<SendMessageResult> sendGroupTypingMessage(
425 final SignalServiceTypingMessage message,
426 final Set<RecipientId> recipientIds,
427 final DistributionId distributionId
428 ) throws IOException {
429 final var messageSender = dependencies.getMessageSender();
430 final var results = sendGroupMessageInternal((recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendTyping(
431 recipients,
432 unidentifiedAccess,
433 message,
434 () -> false),
435 (distId, recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendGroupTyping(distId,
436 recipients,
437 unidentifiedAccess,
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 return g;
458 }
459
460 private List<SendMessageResult> sendGroupMessageInternal(
461 final LegacySenderHandler legacySender,
462 final SenderKeySenderHandler senderKeySender,
463 final Set<RecipientId> recipientIds,
464 final DistributionId distributionId
465 ) throws IOException {
466 long startTime = System.currentTimeMillis();
467 // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false.
468 final var isRecipientUpdate = false;
469 Set<RecipientId> senderKeyTargets = distributionId == null
470 ? Set.of()
471 : getSenderKeyCapableRecipientIds(recipientIds);
472 final var allResults = new ArrayList<SendMessageResult>(recipientIds.size());
473
474 if (!senderKeyTargets.isEmpty()) {
475 final var results = sendGroupMessageInternalWithSenderKey(senderKeySender,
476 senderKeyTargets,
477 distributionId,
478 isRecipientUpdate);
479
480 if (results == null) {
481 senderKeyTargets = Set.of();
482 } else {
483 results.stream().filter(SendMessageResult::isSuccess).forEach(allResults::add);
484 final var recipientResolver = account.getRecipientResolver();
485 final var failedTargets = results.stream()
486 .filter(r -> !r.isSuccess())
487 .map(r -> recipientResolver.resolveRecipient(r.getAddress()))
488 .toList();
489 if (!failedTargets.isEmpty()) {
490 senderKeyTargets = new HashSet<>(senderKeyTargets);
491 failedTargets.forEach(senderKeyTargets::remove);
492 }
493 }
494 }
495
496 final var legacyTargets = new HashSet<>(recipientIds);
497 legacyTargets.removeAll(senderKeyTargets);
498 final boolean onlyTargetIsSelfWithLinkedDevice = recipientIds.isEmpty() && account.isMultiDevice();
499
500 if (!legacyTargets.isEmpty() || onlyTargetIsSelfWithLinkedDevice) {
501 if (!legacyTargets.isEmpty()) {
502 logger.debug("Need to do {} legacy sends.", legacyTargets.size());
503 } else {
504 logger.debug("Need to do a legacy send to send a sync message for a group of only ourselves.");
505 }
506
507 final List<SendMessageResult> results = sendGroupMessageInternalWithLegacy(legacySender,
508 legacyTargets,
509 isRecipientUpdate || !allResults.isEmpty());
510 allResults.addAll(results);
511 }
512 final var duration = Duration.ofMillis(System.currentTimeMillis() - startTime);
513 logger.debug("Sending took {}", duration.toString());
514 return allResults;
515 }
516
517 private Set<RecipientId> getSenderKeyCapableRecipientIds(final Set<RecipientId> recipientIds) {
518 final var selfProfile = context.getProfileHelper().getSelfProfile();
519 if (selfProfile == null || !selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) {
520 logger.debug("Not all of our devices support sender key. Using legacy.");
521 return Set.of();
522 }
523
524 final var senderKeyTargets = new HashSet<RecipientId>();
525 final var recipientList = new ArrayList<>(recipientIds);
526 final var profiles = context.getProfileHelper().getRecipientProfiles(recipientList).iterator();
527 for (final var recipientId : recipientList) {
528 final var profile = profiles.next();
529 if (profile == null || !profile.getCapabilities().contains(Profile.Capability.senderKey)) {
530 continue;
531 }
532
533 final var access = context.getUnidentifiedAccessHelper().getAccessFor(recipientId);
534 if (access.isEmpty() || access.get().getTargetUnidentifiedAccess().isEmpty()) {
535 continue;
536 }
537
538 final var serviceId = account.getRecipientAddressResolver()
539 .resolveRecipientAddress(recipientId)
540 .serviceId()
541 .orElse(null);
542 if (serviceId == null) {
543 continue;
544 }
545 final var identity = account.getIdentityKeyStore().getIdentityInfo(serviceId);
546 if (identity == null || !identity.getTrustLevel().isTrusted()) {
547 continue;
548 }
549
550 senderKeyTargets.add(recipientId);
551 }
552
553 if (senderKeyTargets.size() < 2) {
554 logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
555 return Set.of();
556 }
557
558 logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size());
559 return senderKeyTargets;
560 }
561
562 private List<SendMessageResult> sendGroupMessageInternalWithLegacy(
563 final LegacySenderHandler sender, final Set<RecipientId> recipientIds, final boolean isRecipientUpdate
564 ) throws IOException {
565 final var recipientIdList = new ArrayList<>(recipientIds);
566 final var addresses = recipientIdList.stream()
567 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
568 .toList();
569 final var unidentifiedAccesses = context.getUnidentifiedAccessHelper().getAccessFor(recipientIdList);
570 try {
571 final var results = sender.send(addresses, unidentifiedAccesses, isRecipientUpdate);
572
573 final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
574 logger.debug("Successfully sent using 1:1 to {}/{} legacy targets.", successCount, recipientIdList.size());
575 return results;
576 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
577 return List.of();
578 }
579 }
580
581 private List<SendMessageResult> sendGroupMessageInternalWithSenderKey(
582 final SenderKeySenderHandler sender,
583 final Set<RecipientId> recipientIds,
584 final DistributionId distributionId,
585 final boolean isRecipientUpdate
586 ) throws IOException {
587 final var recipientIdList = new ArrayList<>(recipientIds);
588
589 long keyCreateTime = account.getSenderKeyStore()
590 .getCreateTimeForOurKey(account.getAci(), account.getDeviceId(), distributionId);
591 long keyAge = System.currentTimeMillis() - keyCreateTime;
592
593 if (keyCreateTime != -1 && keyAge > TimeUnit.DAYS.toMillis(14)) {
594 logger.debug("DistributionId {} was created at {} and is {} ms old (~{} days). Rotating.",
595 distributionId,
596 keyCreateTime,
597 keyAge,
598 TimeUnit.MILLISECONDS.toDays(keyAge));
599 account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
600 }
601
602 List<SignalServiceAddress> addresses = recipientIdList.stream()
603 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
604 .toList();
605 List<UnidentifiedAccess> unidentifiedAccesses = context.getUnidentifiedAccessHelper()
606 .getAccessFor(recipientIdList)
607 .stream()
608 .map(Optional::get)
609 .map(UnidentifiedAccessPair::getTargetUnidentifiedAccess)
610 .map(Optional::get)
611 .toList();
612
613 try {
614 List<SendMessageResult> results = sender.send(distributionId,
615 addresses,
616 unidentifiedAccesses,
617 isRecipientUpdate);
618
619 final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
620 logger.debug("Successfully sent using sender key to {}/{} sender key targets.",
621 successCount,
622 addresses.size());
623
624 return results;
625 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
626 return null;
627 } catch (InvalidUnidentifiedAccessHeaderException e) {
628 logger.warn("Someone had a bad UD header. Falling back to legacy sends.", e);
629 return null;
630 } catch (NoSessionException e) {
631 logger.warn("No session. Falling back to legacy sends.", e);
632 account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
633 return null;
634 } catch (InvalidKeyException e) {
635 logger.warn("Invalid key. Falling back to legacy sends.", e);
636 account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
637 return null;
638 } catch (InvalidRegistrationIdException e) {
639 logger.warn("Invalid registrationId. Falling back to legacy sends.", e);
640 return null;
641 } catch (NotFoundException e) {
642 logger.warn("Someone was unregistered. Falling back to legacy sends.", e);
643 return null;
644 } catch (IOException e) {
645 if (e.getCause() instanceof InvalidKeyException) {
646 logger.warn("Invalid key. Falling back to legacy sends.", e);
647 return null;
648 } else {
649 throw e;
650 }
651 }
652 }
653
654 private SendMessageResult sendMessage(
655 SignalServiceDataMessage message, RecipientId recipientId, Optional<Long> editTargetTimestamp
656 ) {
657 final var messageSendLogStore = account.getMessageSendLogStore();
658 final var urgent = true;
659 final var includePniSignature = false;
660 final var result = handleSendMessage(recipientId,
661 editTargetTimestamp.isEmpty()
662 ? (messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address,
663 unidentifiedAccess,
664 ContentHint.RESENDABLE,
665 message,
666 SignalServiceMessageSender.IndividualSendEvents.EMPTY,
667 urgent,
668 includePniSignature)
669 : (messageSender, address, unidentifiedAccess) -> messageSender.sendEditMessage(address,
670 unidentifiedAccess,
671 ContentHint.RESENDABLE,
672 message,
673 SignalServiceMessageSender.IndividualSendEvents.EMPTY,
674 urgent,
675 editTargetTimestamp.get()));
676 messageSendLogStore.insertIfPossible(message.getTimestamp(), result, ContentHint.RESENDABLE, urgent);
677 handleSendMessageResult(result);
678 return result;
679 }
680
681 private SendMessageResult handleSendMessage(RecipientId recipientId, SenderHandler s) {
682 var messageSender = dependencies.getMessageSender();
683
684 var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
685 try {
686 try {
687 return s.send(messageSender, address, context.getUnidentifiedAccessHelper().getAccessFor(recipientId));
688 } catch (UnregisteredUserException e) {
689 final RecipientId newRecipientId;
690 try {
691 newRecipientId = context.getRecipientHelper().refreshRegisteredUser(recipientId);
692 } catch (UnregisteredRecipientException ex) {
693 return SendMessageResult.unregisteredFailure(address);
694 }
695 address = context.getRecipientHelper().resolveSignalServiceAddress(newRecipientId);
696 return s.send(messageSender,
697 address,
698 context.getUnidentifiedAccessHelper().getAccessFor(newRecipientId));
699 }
700 } catch (UnregisteredUserException e) {
701 return SendMessageResult.unregisteredFailure(address);
702 } catch (ProofRequiredException e) {
703 return SendMessageResult.proofRequiredFailure(address, e);
704 } catch (RateLimitException e) {
705 logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage());
706 return SendMessageResult.rateLimitFailure(address, e);
707 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
708 return SendMessageResult.identityFailure(address, e.getIdentityKey());
709 } catch (IOException e) {
710 logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
711 logger.debug("Exception", e);
712 return SendMessageResult.networkFailure(address);
713 }
714 }
715
716 private SendMessageResult sendSelfMessage(SignalServiceDataMessage message, Optional<Long> editTargetTimestamp) {
717 var address = account.getSelfAddress();
718 var transcript = new SentTranscriptMessage(Optional.of(address),
719 message.getTimestamp(),
720 editTargetTimestamp.isEmpty() ? Optional.of(message) : Optional.empty(),
721 message.getExpiresInSeconds(),
722 Map.of(address.getServiceId(), true),
723 false,
724 Optional.empty(),
725 Set.of(),
726 editTargetTimestamp.map((timestamp) -> new SignalServiceEditMessage(timestamp, message)));
727 var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
728
729 return sendSyncMessage(syncMessage);
730 }
731
732 private void handleSendMessageResult(final SendMessageResult r) {
733 if (r.isSuccess() && !r.getSuccess().isUnidentified()) {
734 final var recipientId = account.getRecipientResolver().resolveRecipient(r.getAddress());
735 final var profile = account.getProfileStore().getProfile(recipientId);
736 if (profile != null && (
737 profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.ENABLED
738 || profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED
739 )) {
740 account.getProfileStore()
741 .storeProfile(recipientId,
742 Profile.newBuilder(profile)
743 .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN)
744 .build());
745 }
746 }
747 if (r.isUnregisteredFailure()) {
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.getIdentityFailure() != null) {
762 final var recipientId = account.getRecipientResolver().resolveRecipient(r.getAddress());
763 context.getIdentityHelper()
764 .handleIdentityFailure(recipientId, r.getAddress().getServiceId(), r.getIdentityFailure());
765 }
766 }
767
768 interface SenderHandler {
769
770 SendMessageResult send(
771 SignalServiceMessageSender messageSender,
772 SignalServiceAddress address,
773 Optional<UnidentifiedAccessPair> unidentifiedAccess
774 ) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
775 }
776
777 interface SenderKeySenderHandler {
778
779 List<SendMessageResult> send(
780 DistributionId distributionId,
781 List<SignalServiceAddress> recipients,
782 List<UnidentifiedAccess> unidentifiedAccess,
783 boolean isRecipientUpdate
784 ) throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException;
785 }
786
787 interface LegacySenderHandler {
788
789 List<SendMessageResult> send(
790 List<SignalServiceAddress> recipients,
791 List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
792 boolean isRecipientUpdate
793 ) throws IOException, UntrustedIdentityException;
794 }
795 }