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