]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java
237f813d36f9e5d04a477bfbcb5852d62ab59178
[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.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;
30
31 import java.io.IOException;
32 import java.util.ArrayList;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Set;
36 import java.util.stream.Collectors;
37
38 public class SendHelper {
39
40 private final static Logger logger = LoggerFactory.getLogger(SendHelper.class);
41
42 private final SignalAccount account;
43 private final SignalDependencies dependencies;
44 private final UnidentifiedAccessHelper unidentifiedAccessHelper;
45 private final SignalServiceAddressResolver addressResolver;
46 private final RecipientResolver recipientResolver;
47 private final IdentityFailureHandler identityFailureHandler;
48 private final GroupProvider groupProvider;
49 private final RecipientRegistrationRefresher recipientRegistrationRefresher;
50
51 public SendHelper(
52 final SignalAccount account,
53 final SignalDependencies dependencies,
54 final UnidentifiedAccessHelper unidentifiedAccessHelper,
55 final SignalServiceAddressResolver addressResolver,
56 final RecipientResolver recipientResolver,
57 final IdentityFailureHandler identityFailureHandler,
58 final GroupProvider groupProvider,
59 final RecipientRegistrationRefresher recipientRegistrationRefresher
60 ) {
61 this.account = account;
62 this.dependencies = dependencies;
63 this.unidentifiedAccessHelper = unidentifiedAccessHelper;
64 this.addressResolver = addressResolver;
65 this.recipientResolver = recipientResolver;
66 this.identityFailureHandler = identityFailureHandler;
67 this.groupProvider = groupProvider;
68 this.recipientRegistrationRefresher = recipientRegistrationRefresher;
69 }
70
71 /**
72 * Send a single message to one recipient.
73 * The message is extended with the current expiration timer.
74 */
75 public SendMessageResult sendMessage(
76 final SignalServiceDataMessage.Builder messageBuilder, final RecipientId recipientId
77 ) throws IOException {
78 final var contact = account.getContactStore().getContact(recipientId);
79 final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
80 messageBuilder.withExpiration(expirationTime);
81 messageBuilder.withProfileKey(account.getProfileKey().serialize());
82
83 final var message = messageBuilder.build();
84 final var result = sendMessage(message, recipientId);
85 handlePossibleIdentityFailure(result);
86 return result;
87 }
88
89 /**
90 * Send a group message to the given group
91 * The message is extended with the current expiration timer for the group and the group context.
92 */
93 public List<SendMessageResult> sendAsGroupMessage(
94 SignalServiceDataMessage.Builder messageBuilder, GroupId groupId
95 ) throws IOException, GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException {
96 final var g = getGroupForSending(groupId);
97 return sendAsGroupMessage(messageBuilder, g);
98 }
99
100 private List<SendMessageResult> sendAsGroupMessage(
101 final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g
102 ) throws IOException, GroupSendingNotAllowedException {
103 GroupUtils.setGroupContext(messageBuilder, g);
104 messageBuilder.withExpiration(g.getMessageExpirationTimer());
105
106 final var message = messageBuilder.build();
107 final var recipients = g.getMembersWithout(account.getSelfRecipientId());
108
109 if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
110 if (message.getBody().isPresent()
111 || message.getAttachments().isPresent()
112 || message.getQuote().isPresent()
113 || message.getPreviews().isPresent()
114 || message.getMentions().isPresent()
115 || message.getSticker().isPresent()) {
116 throw new GroupSendingNotAllowedException(g.getGroupId(), g.getTitle());
117 }
118 }
119
120 return sendGroupMessage(message, recipients);
121 }
122
123 /**
124 * Send a complete group message to the given recipients (should be current/old/new members)
125 * This method should only be used for create/update/quit group messages.
126 */
127 public List<SendMessageResult> sendGroupMessage(
128 final SignalServiceDataMessage message, final Set<RecipientId> recipientIds
129 ) throws IOException {
130 List<SendMessageResult> result = sendGroupMessageInternal(message, recipientIds);
131
132 for (var r : result) {
133 handlePossibleIdentityFailure(r);
134 }
135
136 return result;
137 }
138
139 public SendMessageResult sendDeliveryReceipt(
140 RecipientId recipientId, List<Long> messageIds
141 ) {
142 var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY,
143 messageIds,
144 System.currentTimeMillis());
145
146 return sendReceiptMessage(receiptMessage, recipientId);
147 }
148
149 public SendMessageResult sendReceiptMessage(
150 final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId
151 ) {
152 return handleSendMessage(recipientId,
153 (messageSender, address, unidentifiedAccess) -> messageSender.sendReceipt(address,
154 unidentifiedAccess,
155 receiptMessage));
156 }
157
158 public SendMessageResult sendRetryReceipt(
159 DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional<GroupId> groupId
160 ) {
161 logger.debug("Sending retry receipt for {} to {}, device: {}",
162 errorMessage.getTimestamp(),
163 recipientId,
164 errorMessage.getDeviceId());
165 return handleSendMessage(recipientId,
166 (messageSender, address, unidentifiedAccess) -> messageSender.sendRetryReceipt(address,
167 unidentifiedAccess,
168 groupId.transform(GroupId::serialize),
169 errorMessage));
170 }
171
172 public SendMessageResult sendNullMessage(RecipientId recipientId) {
173 return handleSendMessage(recipientId, SignalServiceMessageSender::sendNullMessage);
174 }
175
176 public SendMessageResult sendSelfMessage(
177 SignalServiceDataMessage.Builder messageBuilder
178 ) {
179 final var recipientId = account.getSelfRecipientId();
180 final var contact = account.getContactStore().getContact(recipientId);
181 final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
182 messageBuilder.withExpiration(expirationTime);
183
184 var message = messageBuilder.build();
185 return sendSelfMessage(message);
186 }
187
188 public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message) {
189 var messageSender = dependencies.getMessageSender();
190 try {
191 return messageSender.sendSyncMessage(message, unidentifiedAccessHelper.getAccessForSync());
192 } catch (UnregisteredUserException e) {
193 var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId());
194 return SendMessageResult.unregisteredFailure(address);
195 } catch (ProofRequiredException e) {
196 var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId());
197 return SendMessageResult.proofRequiredFailure(address, e);
198 } catch (RateLimitException e) {
199 var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId());
200 logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage());
201 return SendMessageResult.networkFailure(address);
202 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
203 var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId());
204 return SendMessageResult.identityFailure(address, e.getIdentityKey());
205 } catch (IOException e) {
206 var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId());
207 logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
208 return SendMessageResult.networkFailure(address);
209 }
210 }
211
212 public SendMessageResult sendTypingMessage(
213 SignalServiceTypingMessage message, RecipientId recipientId
214 ) {
215 return handleSendMessage(recipientId,
216 (messageSender, address, unidentifiedAccess) -> messageSender.sendTyping(address,
217 unidentifiedAccess,
218 message));
219 }
220
221 public List<SendMessageResult> sendGroupTypingMessage(
222 SignalServiceTypingMessage message, GroupId groupId
223 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
224 final var g = getGroupForSending(groupId);
225 if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
226 throw new GroupSendingNotAllowedException(groupId, g.getTitle());
227 }
228 final var messageSender = dependencies.getMessageSender();
229 final var recipientIdList = new ArrayList<>(g.getMembersWithout(account.getSelfRecipientId()));
230 final var addresses = recipientIdList.stream()
231 .map(addressResolver::resolveSignalServiceAddress)
232 .collect(Collectors.toList());
233 return messageSender.sendTyping(addresses,
234 unidentifiedAccessHelper.getAccessFor(recipientIdList),
235 message,
236 null);
237 }
238
239 private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
240 var g = groupProvider.getGroup(groupId);
241 if (g == null) {
242 throw new GroupNotFoundException(groupId);
243 }
244 if (!g.isMember(account.getSelfRecipientId())) {
245 throw new NotAGroupMemberException(groupId, g.getTitle());
246 }
247 return g;
248 }
249
250 private List<SendMessageResult> sendGroupMessageInternal(
251 final SignalServiceDataMessage message, final Set<RecipientId> recipientIds
252 ) throws IOException {
253 try {
254 var messageSender = dependencies.getMessageSender();
255 // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false.
256 final var isRecipientUpdate = false;
257 final var recipientIdList = new ArrayList<>(recipientIds);
258 final var addresses = recipientIdList.stream()
259 .map(addressResolver::resolveSignalServiceAddress)
260 .collect(Collectors.toList());
261 return messageSender.sendDataMessage(addresses,
262 unidentifiedAccessHelper.getAccessFor(recipientIdList),
263 isRecipientUpdate,
264 ContentHint.DEFAULT,
265 message,
266 SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
267 sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()),
268 () -> false);
269 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
270 return List.of();
271 }
272 }
273
274 private SendMessageResult sendMessage(
275 SignalServiceDataMessage message, RecipientId recipientId
276 ) {
277 return handleSendMessage(recipientId,
278 (messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address,
279 unidentifiedAccess,
280 ContentHint.DEFAULT,
281 message,
282 SignalServiceMessageSender.IndividualSendEvents.EMPTY));
283 }
284
285 private SendMessageResult handleSendMessage(RecipientId recipientId, SenderHandler s) {
286 var messageSender = dependencies.getMessageSender();
287
288 var address = addressResolver.resolveSignalServiceAddress(recipientId);
289 try {
290 try {
291 return s.send(messageSender, address, unidentifiedAccessHelper.getAccessFor(recipientId));
292 } catch (UnregisteredUserException e) {
293 final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId);
294 address = addressResolver.resolveSignalServiceAddress(newRecipientId);
295 return s.send(messageSender, address, unidentifiedAccessHelper.getAccessFor(newRecipientId));
296 }
297 } catch (UnregisteredUserException e) {
298 return SendMessageResult.unregisteredFailure(address);
299 } catch (ProofRequiredException e) {
300 return SendMessageResult.proofRequiredFailure(address, e);
301 } catch (RateLimitException e) {
302 logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage());
303 return SendMessageResult.networkFailure(address);
304 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
305 return SendMessageResult.identityFailure(address, e.getIdentityKey());
306 } catch (IOException e) {
307 logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
308 return SendMessageResult.networkFailure(address);
309 }
310 }
311
312 private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) {
313 var address = account.getSelfAddress();
314 var transcript = new SentTranscriptMessage(Optional.of(address),
315 message.getTimestamp(),
316 message,
317 message.getExpiresInSeconds(),
318 Map.of(address, true),
319 false);
320 var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
321
322 return sendSyncMessage(syncMessage);
323 }
324
325 private void handlePossibleIdentityFailure(final SendMessageResult r) {
326 if (r.getIdentityFailure() != null) {
327 final var recipientId = recipientResolver.resolveRecipient(r.getAddress());
328 identityFailureHandler.handleIdentityFailure(recipientId, r.getIdentityFailure());
329 }
330 }
331
332 interface SenderHandler {
333
334 SendMessageResult send(
335 SignalServiceMessageSender messageSender,
336 SignalServiceAddress address,
337 Optional<UnidentifiedAccessPair> unidentifiedAccess
338 ) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
339 }
340 }