1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.SignalDependencies
;
4 import org
.asamk
.signal
.manager
.groups
.GroupId
;
5 import org
.asamk
.signal
.manager
.groups
.GroupNotFoundException
;
6 import org
.asamk
.signal
.manager
.groups
.GroupSendingNotAllowedException
;
7 import org
.asamk
.signal
.manager
.groups
.GroupUtils
;
8 import org
.asamk
.signal
.manager
.groups
.NotAGroupMemberException
;
9 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
10 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfo
;
11 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
12 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientResolver
;
13 import org
.slf4j
.Logger
;
14 import org
.slf4j
.LoggerFactory
;
15 import org
.whispersystems
.libsignal
.protocol
.DecryptionErrorMessage
;
16 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
17 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageSender
;
18 import org
.whispersystems
.signalservice
.api
.crypto
.ContentHint
;
19 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccessPair
;
20 import org
.whispersystems
.signalservice
.api
.messages
.SendMessageResult
;
21 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
22 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceReceiptMessage
;
23 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceTypingMessage
;
24 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SentTranscriptMessage
;
25 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
26 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
27 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.ProofRequiredException
;
28 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.RateLimitException
;
29 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.UnregisteredUserException
;
31 import java
.io
.IOException
;
32 import java
.util
.ArrayList
;
33 import java
.util
.List
;
37 public class SendHelper
{
39 private final static Logger logger
= LoggerFactory
.getLogger(SendHelper
.class);
41 private final SignalAccount account
;
42 private final SignalDependencies dependencies
;
43 private final UnidentifiedAccessHelper unidentifiedAccessHelper
;
44 private final SignalServiceAddressResolver addressResolver
;
45 private final RecipientResolver recipientResolver
;
46 private final IdentityFailureHandler identityFailureHandler
;
47 private final GroupProvider groupProvider
;
48 private final RecipientRegistrationRefresher recipientRegistrationRefresher
;
51 final SignalAccount account
,
52 final SignalDependencies dependencies
,
53 final UnidentifiedAccessHelper unidentifiedAccessHelper
,
54 final SignalServiceAddressResolver addressResolver
,
55 final RecipientResolver recipientResolver
,
56 final IdentityFailureHandler identityFailureHandler
,
57 final GroupProvider groupProvider
,
58 final RecipientRegistrationRefresher recipientRegistrationRefresher
60 this.account
= account
;
61 this.dependencies
= dependencies
;
62 this.unidentifiedAccessHelper
= unidentifiedAccessHelper
;
63 this.addressResolver
= addressResolver
;
64 this.recipientResolver
= recipientResolver
;
65 this.identityFailureHandler
= identityFailureHandler
;
66 this.groupProvider
= groupProvider
;
67 this.recipientRegistrationRefresher
= recipientRegistrationRefresher
;
71 * Send a single message to one recipient.
72 * The message is extended with the current expiration timer.
74 public SendMessageResult
sendMessage(
75 final SignalServiceDataMessage
.Builder messageBuilder
, final RecipientId recipientId
76 ) throws IOException
{
77 final var contact
= account
.getContactStore().getContact(recipientId
);
78 final var expirationTime
= contact
!= null ? contact
.getMessageExpirationTime() : 0;
79 messageBuilder
.withExpiration(expirationTime
);
80 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
82 final var message
= messageBuilder
.build();
83 final var result
= sendMessage(message
, recipientId
);
84 handlePossibleIdentityFailure(result
);
89 * Send a group message to the given group
90 * The message is extended with the current expiration timer for the group and the group context.
92 public List
<SendMessageResult
> sendAsGroupMessage(
93 SignalServiceDataMessage
.Builder messageBuilder
, GroupId groupId
94 ) throws IOException
, GroupNotFoundException
, NotAGroupMemberException
, GroupSendingNotAllowedException
{
95 final var g
= getGroupForSending(groupId
);
96 return sendAsGroupMessage(messageBuilder
, g
);
99 private List
<SendMessageResult
> sendAsGroupMessage(
100 final SignalServiceDataMessage
.Builder messageBuilder
, final GroupInfo g
101 ) throws IOException
, GroupSendingNotAllowedException
{
102 GroupUtils
.setGroupContext(messageBuilder
, g
);
103 messageBuilder
.withExpiration(g
.getMessageExpirationTimer());
105 final var message
= messageBuilder
.build();
106 final var recipients
= g
.getMembersWithout(account
.getSelfRecipientId());
108 if (g
.isAnnouncementGroup() && !g
.isAdmin(account
.getSelfRecipientId())) {
109 if (message
.getBody().isPresent()
110 || message
.getAttachments().isPresent()
111 || message
.getQuote().isPresent()
112 || message
.getPreviews().isPresent()
113 || message
.getMentions().isPresent()
114 || message
.getSticker().isPresent()) {
115 throw new GroupSendingNotAllowedException(g
.getGroupId(), g
.getTitle());
119 return sendGroupMessage(message
, recipients
);
123 * Send a complete group message to the given recipients (should be current/old/new members)
124 * This method should only be used for create/update/quit group messages.
126 public List
<SendMessageResult
> sendGroupMessage(
127 final SignalServiceDataMessage message
, final Set
<RecipientId
> recipientIds
128 ) throws IOException
{
129 List
<SendMessageResult
> result
= sendGroupMessageInternal(message
, recipientIds
);
131 for (var r
: result
) {
132 handlePossibleIdentityFailure(r
);
138 public SendMessageResult
sendDeliveryReceipt(
139 RecipientId recipientId
, List
<Long
> messageIds
141 var receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
143 System
.currentTimeMillis());
145 return sendReceiptMessage(receiptMessage
, recipientId
);
148 public SendMessageResult
sendReceiptMessage(
149 final SignalServiceReceiptMessage receiptMessage
, final RecipientId recipientId
151 return handleSendMessage(recipientId
,
152 (messageSender
, address
, unidentifiedAccess
) -> messageSender
.sendReceipt(address
,
157 public SendMessageResult
sendRetryReceipt(
158 DecryptionErrorMessage errorMessage
, RecipientId recipientId
, Optional
<GroupId
> groupId
160 logger
.debug("Sending retry receipt for {} to {}, device: {}",
161 errorMessage
.getTimestamp(),
163 errorMessage
.getDeviceId());
164 return handleSendMessage(recipientId
,
165 (messageSender
, address
, unidentifiedAccess
) -> messageSender
.sendRetryReceipt(address
,
167 groupId
.transform(GroupId
::serialize
),
171 public SendMessageResult
sendNullMessage(RecipientId recipientId
) {
172 return handleSendMessage(recipientId
, SignalServiceMessageSender
::sendNullMessage
);
175 public SendMessageResult
sendSelfMessage(
176 SignalServiceDataMessage
.Builder messageBuilder
178 final var recipientId
= account
.getSelfRecipientId();
179 final var contact
= account
.getContactStore().getContact(recipientId
);
180 final var expirationTime
= contact
!= null ? contact
.getMessageExpirationTime() : 0;
181 messageBuilder
.withExpiration(expirationTime
);
183 var message
= messageBuilder
.build();
184 return sendSelfMessage(message
);
187 public SendMessageResult
sendSyncMessage(SignalServiceSyncMessage message
) {
188 var messageSender
= dependencies
.getMessageSender();
190 return messageSender
.sendSyncMessage(message
, unidentifiedAccessHelper
.getAccessForSync());
191 } catch (UnregisteredUserException e
) {
192 var address
= addressResolver
.resolveSignalServiceAddress(account
.getSelfRecipientId());
193 return SendMessageResult
.unregisteredFailure(address
);
194 } catch (ProofRequiredException e
) {
195 var address
= addressResolver
.resolveSignalServiceAddress(account
.getSelfRecipientId());
196 return SendMessageResult
.proofRequiredFailure(address
, e
);
197 } catch (RateLimitException e
) {
198 var address
= addressResolver
.resolveSignalServiceAddress(account
.getSelfRecipientId());
199 logger
.warn("Sending failed due to rate limiting from the signal server: {}", e
.getMessage());
200 return SendMessageResult
.networkFailure(address
);
201 } catch (org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException e
) {
202 var address
= addressResolver
.resolveSignalServiceAddress(account
.getSelfRecipientId());
203 return SendMessageResult
.identityFailure(address
, e
.getIdentityKey());
204 } catch (IOException e
) {
205 var address
= addressResolver
.resolveSignalServiceAddress(account
.getSelfRecipientId());
206 logger
.warn("Failed to send message due to IO exception: {}", e
.getMessage());
207 return SendMessageResult
.networkFailure(address
);
211 public SendMessageResult
sendTypingMessage(
212 SignalServiceTypingMessage message
, RecipientId recipientId
214 return handleSendMessage(recipientId
,
215 (messageSender
, address
, unidentifiedAccess
) -> messageSender
.sendTyping(address
,
220 public List
<SendMessageResult
> sendGroupTypingMessage(
221 SignalServiceTypingMessage message
, GroupId groupId
222 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, GroupSendingNotAllowedException
{
223 final var g
= getGroupForSending(groupId
);
224 if (g
.isAnnouncementGroup() && !g
.isAdmin(account
.getSelfRecipientId())) {
225 throw new GroupSendingNotAllowedException(groupId
, g
.getTitle());
227 final var messageSender
= dependencies
.getMessageSender();
228 final var recipientIdList
= new ArrayList
<>(g
.getMembersWithout(account
.getSelfRecipientId()));
229 final var addresses
= recipientIdList
.stream().map(addressResolver
::resolveSignalServiceAddress
).toList();
230 return messageSender
.sendTyping(addresses
,
231 unidentifiedAccessHelper
.getAccessFor(recipientIdList
),
236 private GroupInfo
getGroupForSending(GroupId groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
237 var g
= groupProvider
.getGroup(groupId
);
239 throw new GroupNotFoundException(groupId
);
241 if (!g
.isMember(account
.getSelfRecipientId())) {
242 throw new NotAGroupMemberException(groupId
, g
.getTitle());
247 private List
<SendMessageResult
> sendGroupMessageInternal(
248 final SignalServiceDataMessage message
, final Set
<RecipientId
> recipientIds
249 ) throws IOException
{
251 var messageSender
= dependencies
.getMessageSender();
252 // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false.
253 final var isRecipientUpdate
= false;
254 final var recipientIdList
= new ArrayList
<>(recipientIds
);
255 final var addresses
= recipientIdList
.stream().map(addressResolver
::resolveSignalServiceAddress
).toList();
256 return messageSender
.sendDataMessage(addresses
,
257 unidentifiedAccessHelper
.getAccessFor(recipientIdList
),
261 SignalServiceMessageSender
.LegacyGroupEvents
.EMPTY
,
262 sendResult
-> logger
.trace("Partial message send result: {}", sendResult
.isSuccess()),
264 } catch (org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException e
) {
269 private SendMessageResult
sendMessage(
270 SignalServiceDataMessage message
, RecipientId recipientId
272 return handleSendMessage(recipientId
,
273 (messageSender
, address
, unidentifiedAccess
) -> messageSender
.sendDataMessage(address
,
277 SignalServiceMessageSender
.IndividualSendEvents
.EMPTY
));
280 private SendMessageResult
handleSendMessage(RecipientId recipientId
, SenderHandler s
) {
281 var messageSender
= dependencies
.getMessageSender();
283 var address
= addressResolver
.resolveSignalServiceAddress(recipientId
);
286 return s
.send(messageSender
, address
, unidentifiedAccessHelper
.getAccessFor(recipientId
));
287 } catch (UnregisteredUserException e
) {
288 final var newRecipientId
= recipientRegistrationRefresher
.refreshRecipientRegistration(recipientId
);
289 address
= addressResolver
.resolveSignalServiceAddress(newRecipientId
);
290 return s
.send(messageSender
, address
, unidentifiedAccessHelper
.getAccessFor(newRecipientId
));
292 } catch (UnregisteredUserException e
) {
293 return SendMessageResult
.unregisteredFailure(address
);
294 } catch (ProofRequiredException e
) {
295 return SendMessageResult
.proofRequiredFailure(address
, e
);
296 } catch (RateLimitException e
) {
297 logger
.warn("Sending failed due to rate limiting from the signal server: {}", e
.getMessage());
298 return SendMessageResult
.networkFailure(address
);
299 } catch (org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException e
) {
300 return SendMessageResult
.identityFailure(address
, e
.getIdentityKey());
301 } catch (IOException e
) {
302 logger
.warn("Failed to send message due to IO exception: {}", e
.getMessage());
303 return SendMessageResult
.networkFailure(address
);
307 private SendMessageResult
sendSelfMessage(SignalServiceDataMessage message
) {
308 var address
= account
.getSelfAddress();
309 var transcript
= new SentTranscriptMessage(Optional
.of(address
),
310 message
.getTimestamp(),
312 message
.getExpiresInSeconds(),
313 Map
.of(address
, true),
315 var syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
317 return sendSyncMessage(syncMessage
);
320 private void handlePossibleIdentityFailure(final SendMessageResult r
) {
321 if (r
.getIdentityFailure() != null) {
322 final var recipientId
= recipientResolver
.resolveRecipient(r
.getAddress());
323 identityFailureHandler
.handleIdentityFailure(recipientId
, r
.getIdentityFailure());
327 interface SenderHandler
{
329 SendMessageResult
send(
330 SignalServiceMessageSender messageSender
,
331 SignalServiceAddress address
,
332 Optional
<UnidentifiedAccessPair
> unidentifiedAccess
333 ) throws IOException
, UnregisteredUserException
, ProofRequiredException
, RateLimitException
, org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException
;