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