1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.SignalDependencies
;
4 import org
.asamk
.signal
.manager
.api
.UnregisteredRecipientException
;
5 import org
.asamk
.signal
.manager
.groups
.GroupId
;
6 import org
.asamk
.signal
.manager
.groups
.GroupNotFoundException
;
7 import org
.asamk
.signal
.manager
.groups
.GroupSendingNotAllowedException
;
8 import org
.asamk
.signal
.manager
.groups
.GroupUtils
;
9 import org
.asamk
.signal
.manager
.groups
.NotAGroupMemberException
;
10 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
11 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfo
;
12 import org
.asamk
.signal
.manager
.storage
.recipients
.Profile
;
13 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
14 import org
.slf4j
.Logger
;
15 import org
.slf4j
.LoggerFactory
;
16 import org
.whispersystems
.libsignal
.InvalidKeyException
;
17 import org
.whispersystems
.libsignal
.InvalidRegistrationIdException
;
18 import org
.whispersystems
.libsignal
.NoSessionException
;
19 import org
.whispersystems
.libsignal
.protocol
.DecryptionErrorMessage
;
20 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
21 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageSender
;
22 import org
.whispersystems
.signalservice
.api
.crypto
.ContentHint
;
23 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccess
;
24 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccessPair
;
25 import org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException
;
26 import org
.whispersystems
.signalservice
.api
.messages
.SendMessageResult
;
27 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
28 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceReceiptMessage
;
29 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceTypingMessage
;
30 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SentTranscriptMessage
;
31 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
32 import org
.whispersystems
.signalservice
.api
.push
.DistributionId
;
33 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
34 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NotFoundException
;
35 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.ProofRequiredException
;
36 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.RateLimitException
;
37 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.UnregisteredUserException
;
38 import org
.whispersystems
.signalservice
.internal
.push
.exceptions
.InvalidUnidentifiedAccessHeaderException
;
40 import java
.io
.IOException
;
41 import java
.util
.ArrayList
;
42 import java
.util
.HashSet
;
43 import java
.util
.List
;
46 import java
.util
.concurrent
.TimeUnit
;
47 import java
.util
.stream
.Collectors
;
49 public class SendHelper
{
51 private final static Logger logger
= LoggerFactory
.getLogger(SendHelper
.class);
53 private final SignalAccount account
;
54 private final SignalDependencies dependencies
;
55 private final Context context
;
57 public SendHelper(final Context context
) {
58 this.account
= context
.getAccount();
59 this.dependencies
= context
.getDependencies();
60 this.context
= context
;
64 * Send a single message to one recipient.
65 * The message is extended with the current expiration timer.
67 public SendMessageResult
sendMessage(
68 final SignalServiceDataMessage
.Builder messageBuilder
, final RecipientId recipientId
69 ) throws IOException
{
70 final var contact
= account
.getContactStore().getContact(recipientId
);
71 final var expirationTime
= contact
!= null ? contact
.getMessageExpirationTime() : 0;
72 messageBuilder
.withExpiration(expirationTime
);
73 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
75 final var message
= messageBuilder
.build();
76 final var result
= sendMessage(message
, recipientId
);
77 handleSendMessageResult(result
);
82 * Send a group message to the given group
83 * The message is extended with the current expiration timer for the group and the group context.
85 public List
<SendMessageResult
> sendAsGroupMessage(
86 SignalServiceDataMessage
.Builder messageBuilder
, GroupId groupId
87 ) throws IOException
, GroupNotFoundException
, NotAGroupMemberException
, GroupSendingNotAllowedException
{
88 final var g
= getGroupForSending(groupId
);
89 return sendAsGroupMessage(messageBuilder
, g
);
92 private List
<SendMessageResult
> sendAsGroupMessage(
93 final SignalServiceDataMessage
.Builder messageBuilder
, final GroupInfo g
94 ) throws IOException
, GroupSendingNotAllowedException
{
95 GroupUtils
.setGroupContext(messageBuilder
, g
);
96 messageBuilder
.withExpiration(g
.getMessageExpirationTimer());
98 final var message
= messageBuilder
.build();
99 final var recipients
= g
.getMembersWithout(account
.getSelfRecipientId());
101 if (g
.isAnnouncementGroup() && !g
.isAdmin(account
.getSelfRecipientId())) {
102 if (message
.getBody().isPresent()
103 || message
.getAttachments().isPresent()
104 || message
.getQuote().isPresent()
105 || message
.getPreviews().isPresent()
106 || message
.getMentions().isPresent()
107 || message
.getSticker().isPresent()) {
108 throw new GroupSendingNotAllowedException(g
.getGroupId(), g
.getTitle());
112 return sendGroupMessage(message
, recipients
, g
.getDistributionId());
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.
119 public List
<SendMessageResult
> sendGroupMessage(
120 final SignalServiceDataMessage message
,
121 final Set
<RecipientId
> recipientIds
,
122 final DistributionId distributionId
123 ) throws IOException
{
124 final var messageSender
= dependencies
.getMessageSender();
125 final var results
= sendGroupMessageInternal((recipients
, unidentifiedAccess
, isRecipientUpdate
) -> messageSender
.sendDataMessage(
131 SignalServiceMessageSender
.LegacyGroupEvents
.EMPTY
,
132 sendResult
-> logger
.trace("Partial message send result: {}", sendResult
.isSuccess()),
134 (distId
, recipients
, unidentifiedAccess
, isRecipientUpdate
) -> messageSender
.sendGroupDataMessage(distId
,
140 SignalServiceMessageSender
.SenderKeyGroupEvents
.EMPTY
),
144 for (var r
: results
) {
145 handleSendMessageResult(r
);
151 public SendMessageResult
sendDeliveryReceipt(
152 RecipientId recipientId
, List
<Long
> messageIds
154 var receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
156 System
.currentTimeMillis());
158 return sendReceiptMessage(receiptMessage
, recipientId
);
161 public SendMessageResult
sendReceiptMessage(
162 final SignalServiceReceiptMessage receiptMessage
, final RecipientId recipientId
164 return handleSendMessage(recipientId
,
165 (messageSender
, address
, unidentifiedAccess
) -> messageSender
.sendReceipt(address
,
170 public SendMessageResult
sendRetryReceipt(
171 DecryptionErrorMessage errorMessage
, RecipientId recipientId
, Optional
<GroupId
> groupId
173 logger
.debug("Sending retry receipt for {} to {}, device: {}",
174 errorMessage
.getTimestamp(),
176 errorMessage
.getDeviceId());
177 return handleSendMessage(recipientId
,
178 (messageSender
, address
, unidentifiedAccess
) -> messageSender
.sendRetryReceipt(address
,
180 groupId
.transform(GroupId
::serialize
),
184 public SendMessageResult
sendNullMessage(RecipientId recipientId
) {
185 return handleSendMessage(recipientId
, SignalServiceMessageSender
::sendNullMessage
);
188 public SendMessageResult
sendSelfMessage(
189 SignalServiceDataMessage
.Builder messageBuilder
191 final var recipientId
= account
.getSelfRecipientId();
192 final var contact
= account
.getContactStore().getContact(recipientId
);
193 final var expirationTime
= contact
!= null ? contact
.getMessageExpirationTime() : 0;
194 messageBuilder
.withExpiration(expirationTime
);
196 var message
= messageBuilder
.build();
197 return sendSelfMessage(message
);
200 public SendMessageResult
sendSyncMessage(SignalServiceSyncMessage message
) {
201 var messageSender
= dependencies
.getMessageSender();
203 return messageSender
.sendSyncMessage(message
, context
.getUnidentifiedAccessHelper().getAccessForSync());
204 } catch (UnregisteredUserException e
) {
205 var address
= context
.getRecipientHelper().resolveSignalServiceAddress(account
.getSelfRecipientId());
206 return SendMessageResult
.unregisteredFailure(address
);
207 } catch (ProofRequiredException e
) {
208 var address
= context
.getRecipientHelper().resolveSignalServiceAddress(account
.getSelfRecipientId());
209 return SendMessageResult
.proofRequiredFailure(address
, e
);
210 } catch (RateLimitException e
) {
211 var address
= context
.getRecipientHelper().resolveSignalServiceAddress(account
.getSelfRecipientId());
212 logger
.warn("Sending failed due to rate limiting from the signal server: {}", e
.getMessage());
213 return SendMessageResult
.networkFailure(address
);
214 } catch (org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException e
) {
215 var address
= context
.getRecipientHelper().resolveSignalServiceAddress(account
.getSelfRecipientId());
216 return SendMessageResult
.identityFailure(address
, e
.getIdentityKey());
217 } catch (IOException e
) {
218 var address
= context
.getRecipientHelper().resolveSignalServiceAddress(account
.getSelfRecipientId());
219 logger
.warn("Failed to send message due to IO exception: {}", e
.getMessage());
220 return SendMessageResult
.networkFailure(address
);
224 public SendMessageResult
sendTypingMessage(
225 SignalServiceTypingMessage message
, RecipientId recipientId
227 return handleSendMessage(recipientId
,
228 (messageSender
, address
, unidentifiedAccess
) -> messageSender
.sendTyping(address
,
233 public List
<SendMessageResult
> sendGroupTypingMessage(
234 SignalServiceTypingMessage message
, GroupId groupId
235 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, GroupSendingNotAllowedException
{
236 final var g
= getGroupForSending(groupId
);
237 if (g
.isAnnouncementGroup() && !g
.isAdmin(account
.getSelfRecipientId())) {
238 throw new GroupSendingNotAllowedException(groupId
, g
.getTitle());
240 final var distributionId
= g
.getDistributionId();
241 final var recipientIds
= g
.getMembersWithout(account
.getSelfRecipientId());
243 return sendGroupTypingMessage(message
, recipientIds
, distributionId
);
246 private List
<SendMessageResult
> sendGroupTypingMessage(
247 final SignalServiceTypingMessage message
,
248 final Set
<RecipientId
> recipientIds
,
249 final DistributionId distributionId
250 ) throws IOException
{
251 final var messageSender
= dependencies
.getMessageSender();
252 final var results
= sendGroupMessageInternal((recipients
, unidentifiedAccess
, isRecipientUpdate
) -> messageSender
.sendTyping(
257 (distId
, recipients
, unidentifiedAccess
, isRecipientUpdate
) -> messageSender
.sendGroupTyping(distId
,
264 for (var r
: results
) {
265 handleSendMessageResult(r
);
271 private GroupInfo
getGroupForSending(GroupId groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
272 var g
= context
.getGroupHelper().getGroup(groupId
);
274 throw new GroupNotFoundException(groupId
);
276 if (!g
.isMember(account
.getSelfRecipientId())) {
277 throw new NotAGroupMemberException(groupId
, g
.getTitle());
282 private List
<SendMessageResult
> sendGroupMessageInternal(
283 final LegacySenderHandler legacySender
,
284 final SenderKeySenderHandler senderKeySender
,
285 final Set
<RecipientId
> recipientIds
,
286 final DistributionId distributionId
287 ) throws IOException
{
288 // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false.
289 final var isRecipientUpdate
= false;
290 Set
<RecipientId
> senderKeyTargets
= distributionId
== null
292 : getSenderKeyCapableRecipientIds(recipientIds
);
293 final var allResults
= new ArrayList
<SendMessageResult
>(recipientIds
.size());
295 if (senderKeyTargets
.size() > 0) {
296 final var results
= sendGroupMessageInternalWithSenderKey(senderKeySender
,
301 if (results
== null) {
302 senderKeyTargets
= Set
.of();
304 results
.stream().filter(SendMessageResult
::isSuccess
).forEach(allResults
::add
);
305 final var failedTargets
= results
.stream()
306 .filter(r
-> !r
.isSuccess())
307 .map(r
-> context
.getRecipientHelper().resolveRecipient(r
.getAddress()))
309 if (failedTargets
.size() > 0) {
310 senderKeyTargets
= new HashSet
<>(senderKeyTargets
);
311 failedTargets
.forEach(senderKeyTargets
::remove
);
316 final var legacyTargets
= new HashSet
<>(recipientIds
);
317 legacyTargets
.removeAll(senderKeyTargets
);
318 final boolean onlyTargetIsSelfWithLinkedDevice
= recipientIds
.isEmpty() && account
.isMultiDevice();
320 if (legacyTargets
.size() > 0 || onlyTargetIsSelfWithLinkedDevice
) {
321 if (legacyTargets
.size() > 0) {
322 logger
.debug("Need to do {} legacy sends.", legacyTargets
.size());
324 logger
.debug("Need to do a legacy send to send a sync message for a group of only ourselves.");
327 final List
<SendMessageResult
> results
= sendGroupMessageInternalWithLegacy(legacySender
,
329 isRecipientUpdate
|| allResults
.size() > 0);
330 allResults
.addAll(results
);
336 private Set
<RecipientId
> getSenderKeyCapableRecipientIds(final Set
<RecipientId
> recipientIds
) {
337 final var selfProfile
= context
.getProfileHelper().getRecipientProfile(account
.getSelfRecipientId());
338 if (selfProfile
== null || !selfProfile
.getCapabilities().contains(Profile
.Capability
.senderKey
)) {
339 logger
.debug("Not all of our devices support sender key. Using legacy.");
343 final var senderKeyTargets
= new HashSet
<RecipientId
>();
344 final var recipientList
= new ArrayList
<>(recipientIds
);
345 final var profiles
= context
.getProfileHelper().getRecipientProfile(recipientList
).iterator();
346 for (final var recipientId
: recipientList
) {
347 final var profile
= profiles
.next();
348 if (profile
== null || !profile
.getCapabilities().contains(Profile
.Capability
.senderKey
)) {
352 final var access
= context
.getUnidentifiedAccessHelper().getAccessFor(recipientId
);
353 if (!access
.isPresent() || !access
.get().getTargetUnidentifiedAccess().isPresent()) {
357 final var identity
= account
.getIdentityKeyStore().getIdentity(recipientId
);
358 if (identity
== null || !identity
.getTrustLevel().isTrusted()) {
362 senderKeyTargets
.add(recipientId
);
365 if (senderKeyTargets
.size() < 2) {
366 logger
.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets
.size());
370 logger
.debug("Can use sender key for {}/{} recipients.", senderKeyTargets
.size(), recipientIds
.size());
371 return senderKeyTargets
;
374 private List
<SendMessageResult
> sendGroupMessageInternalWithLegacy(
375 final LegacySenderHandler sender
, final Set
<RecipientId
> recipientIds
, final boolean isRecipientUpdate
376 ) throws IOException
{
377 final var recipientIdList
= new ArrayList
<>(recipientIds
);
378 final var addresses
= recipientIdList
.stream()
379 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
381 final var unidentifiedAccesses
= context
.getUnidentifiedAccessHelper().getAccessFor(recipientIdList
);
383 final var results
= sender
.send(addresses
, unidentifiedAccesses
, isRecipientUpdate
);
385 final var successCount
= results
.stream().filter(SendMessageResult
::isSuccess
).count();
386 logger
.debug("Successfully sent using 1:1 to {}/{} legacy targets.", successCount
, recipientIdList
.size());
388 } catch (org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException e
) {
393 private List
<SendMessageResult
> sendGroupMessageInternalWithSenderKey(
394 final SenderKeySenderHandler sender
,
395 final Set
<RecipientId
> recipientIds
,
396 final DistributionId distributionId
,
397 final boolean isRecipientUpdate
398 ) throws IOException
{
399 final var recipientIdList
= new ArrayList
<>(recipientIds
);
401 long keyCreateTime
= account
.getSenderKeyStore()
402 .getCreateTimeForOurKey(account
.getSelfRecipientId(), account
.getDeviceId(), distributionId
);
403 long keyAge
= System
.currentTimeMillis() - keyCreateTime
;
405 if (keyCreateTime
!= -1 && keyAge
> TimeUnit
.DAYS
.toMillis(14)) {
406 logger
.debug("DistributionId {} was created at {} and is {} ms old (~{} days). Rotating.",
410 TimeUnit
.MILLISECONDS
.toDays(keyAge
));
411 account
.getSenderKeyStore().deleteOurKey(account
.getSelfRecipientId(), distributionId
);
414 List
<SignalServiceAddress
> addresses
= recipientIdList
.stream()
415 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
416 .collect(Collectors
.toList());
417 List
<UnidentifiedAccess
> unidentifiedAccesses
= context
.getUnidentifiedAccessHelper()
418 .getAccessFor(recipientIdList
)
421 .map(UnidentifiedAccessPair
::getTargetUnidentifiedAccess
)
423 .collect(Collectors
.toList());
426 List
<SendMessageResult
> results
= sender
.send(distributionId
,
428 unidentifiedAccesses
,
431 final var successCount
= results
.stream().filter(SendMessageResult
::isSuccess
).count();
432 logger
.debug("Successfully sent using sender key to {}/{} sender key targets.",
437 } catch (org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException e
) {
439 } catch (InvalidUnidentifiedAccessHeaderException e
) {
440 logger
.warn("Someone had a bad UD header. Falling back to legacy sends.", e
);
442 } catch (NoSessionException e
) {
443 logger
.warn("No session. Falling back to legacy sends.", e
);
444 account
.getSenderKeyStore().deleteOurKey(account
.getSelfRecipientId(), distributionId
);
446 } catch (InvalidKeyException e
) {
447 logger
.warn("Invalid key. Falling back to legacy sends.", e
);
448 account
.getSenderKeyStore().deleteOurKey(account
.getSelfRecipientId(), distributionId
);
450 } catch (InvalidRegistrationIdException e
) {
451 logger
.warn("Invalid registrationId. Falling back to legacy sends.", e
);
453 } catch (NotFoundException e
) {
454 logger
.warn("Someone was unregistered. Falling back to legacy sends.", e
);
459 private SendMessageResult
sendMessage(
460 SignalServiceDataMessage message
, RecipientId recipientId
462 return handleSendMessage(recipientId
,
463 (messageSender
, address
, unidentifiedAccess
) -> messageSender
.sendDataMessage(address
,
467 SignalServiceMessageSender
.IndividualSendEvents
.EMPTY
));
470 private SendMessageResult
handleSendMessage(RecipientId recipientId
, SenderHandler s
) {
471 var messageSender
= dependencies
.getMessageSender();
473 var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
476 return s
.send(messageSender
, address
, context
.getUnidentifiedAccessHelper().getAccessFor(recipientId
));
477 } catch (UnregisteredUserException e
) {
478 final RecipientId newRecipientId
;
480 newRecipientId
= context
.getRecipientHelper().refreshRegisteredUser(recipientId
);
481 } catch (UnregisteredRecipientException ex
) {
482 return SendMessageResult
.unregisteredFailure(address
);
484 address
= context
.getRecipientHelper().resolveSignalServiceAddress(newRecipientId
);
485 return s
.send(messageSender
,
487 context
.getUnidentifiedAccessHelper().getAccessFor(newRecipientId
));
489 } catch (UnregisteredUserException e
) {
490 return SendMessageResult
.unregisteredFailure(address
);
491 } catch (ProofRequiredException e
) {
492 return SendMessageResult
.proofRequiredFailure(address
, e
);
493 } catch (RateLimitException e
) {
494 logger
.warn("Sending failed due to rate limiting from the signal server: {}", e
.getMessage());
495 return SendMessageResult
.networkFailure(address
);
496 } catch (org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException e
) {
497 return SendMessageResult
.identityFailure(address
, e
.getIdentityKey());
498 } catch (IOException e
) {
499 logger
.warn("Failed to send message due to IO exception: {}", e
.getMessage());
500 return SendMessageResult
.networkFailure(address
);
504 private SendMessageResult
sendSelfMessage(SignalServiceDataMessage message
) {
505 var address
= account
.getSelfAddress();
506 var transcript
= new SentTranscriptMessage(Optional
.of(address
),
507 message
.getTimestamp(),
509 message
.getExpiresInSeconds(),
510 Map
.of(address
, true),
512 var syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
514 return sendSyncMessage(syncMessage
);
517 private void handleSendMessageResult(final SendMessageResult r
) {
518 if (r
.isSuccess() && !r
.getSuccess().isUnidentified()) {
519 final var recipientId
= context
.getRecipientHelper().resolveRecipient(r
.getAddress());
520 final var profile
= account
.getRecipientStore().getProfile(recipientId
);
521 if (profile
!= null && (
522 profile
.getUnidentifiedAccessMode() == Profile
.UnidentifiedAccessMode
.ENABLED
523 || profile
.getUnidentifiedAccessMode() == Profile
.UnidentifiedAccessMode
.UNRESTRICTED
525 account
.getRecipientStore()
526 .storeProfile(recipientId
,
527 Profile
.newBuilder(profile
)
528 .withUnidentifiedAccessMode(Profile
.UnidentifiedAccessMode
.UNKNOWN
)
532 if (r
.isUnregisteredFailure()) {
533 final var recipientId
= context
.getRecipientHelper().resolveRecipient(r
.getAddress());
534 final var profile
= account
.getRecipientStore().getProfile(recipientId
);
535 if (profile
!= null && (
536 profile
.getUnidentifiedAccessMode() == Profile
.UnidentifiedAccessMode
.ENABLED
537 || profile
.getUnidentifiedAccessMode() == Profile
.UnidentifiedAccessMode
.UNRESTRICTED
539 account
.getRecipientStore()
540 .storeProfile(recipientId
,
541 Profile
.newBuilder(profile
)
542 .withUnidentifiedAccessMode(Profile
.UnidentifiedAccessMode
.UNKNOWN
)
546 if (r
.getIdentityFailure() != null) {
547 final var recipientId
= context
.getRecipientHelper().resolveRecipient(r
.getAddress());
548 context
.getIdentityHelper().handleIdentityFailure(recipientId
, r
.getIdentityFailure());
552 interface SenderHandler
{
554 SendMessageResult
send(
555 SignalServiceMessageSender messageSender
,
556 SignalServiceAddress address
,
557 Optional
<UnidentifiedAccessPair
> unidentifiedAccess
558 ) throws IOException
, UnregisteredUserException
, ProofRequiredException
, RateLimitException
, org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException
;
561 interface SenderKeySenderHandler
{
563 List
<SendMessageResult
> send(
564 DistributionId distributionId
,
565 List
<SignalServiceAddress
> recipients
,
566 List
<UnidentifiedAccess
> unidentifiedAccess
,
567 boolean isRecipientUpdate
568 ) throws IOException
, UntrustedIdentityException
, NoSessionException
, InvalidKeyException
, InvalidRegistrationIdException
;
571 interface LegacySenderHandler
{
573 List
<SendMessageResult
> send(
574 List
<SignalServiceAddress
> recipients
,
575 List
<Optional
<UnidentifiedAccessPair
>> unidentifiedAccess
,
576 boolean isRecipientUpdate
577 ) throws IOException
, UntrustedIdentityException
;