]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java
dc912634b11530ed91134ad123aa20966e30f553
[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.UntrustedIdentityException;
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.RecipientId;
13 import org.asamk.signal.manager.storage.recipients.RecipientResolver;
14 import org.slf4j.Logger;
15 import org.slf4j.LoggerFactory;
16 import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
17 import org.whispersystems.libsignal.util.guava.Optional;
18 import org.whispersystems.signalservice.api.SignalServiceMessageSender;
19 import org.whispersystems.signalservice.api.crypto.ContentHint;
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.exceptions.ProofRequiredException;
27 import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
28 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
29
30 import java.io.IOException;
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.stream.Collectors;
36
37 public class SendHelper {
38
39 private final static Logger logger = LoggerFactory.getLogger(SendHelper.class);
40
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;
49
50 public SendHelper(
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
59 ) {
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;
68 }
69
70 /**
71 * Send a single message to one or multiple recipients.
72 * The message is extended with the current expiration timer for each recipient.
73 */
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());
81
82 final var message = messageBuilder.build();
83 final var result = sendMessage(message, recipientId);
84 handlePossibleIdentityFailure(result);
85 return result;
86 }
87
88 /**
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.
91 */
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);
97 }
98
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());
104
105 final var message = messageBuilder.build();
106 final var recipients = g.getMembersWithout(account.getSelfRecipientId());
107
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());
116 }
117 }
118
119 return sendGroupMessage(message, recipients);
120 }
121
122 /**
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.
125 */
126 public List<SendMessageResult> sendGroupMessage(
127 final SignalServiceDataMessage message, final Set<RecipientId> recipientIds
128 ) throws IOException {
129 List<SendMessageResult> result = sendGroupMessageInternal(message, recipientIds);
130
131 for (var r : result) {
132 handlePossibleIdentityFailure(r);
133 }
134
135 return result;
136 }
137
138 public void sendDeliveryReceipt(
139 RecipientId recipientId, List<Long> messageIds
140 ) throws IOException, UntrustedIdentityException {
141 var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY,
142 messageIds,
143 System.currentTimeMillis());
144
145 sendReceiptMessage(receiptMessage, recipientId);
146 }
147
148 public void sendReceiptMessage(
149 final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId
150 ) throws IOException, UntrustedIdentityException {
151 final var messageSender = dependencies.getMessageSender();
152 final var address = addressResolver.resolveSignalServiceAddress(recipientId);
153 try {
154 messageSender.sendReceipt(address, unidentifiedAccessHelper.getAccessFor(recipientId), receiptMessage);
155 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
156 throw new UntrustedIdentityException(account.getRecipientStore().resolveRecipientAddress(recipientId));
157 }
158 }
159
160 public void sendRetryReceipt(
161 DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional<GroupId> groupId
162 ) throws IOException, UntrustedIdentityException {
163 var messageSender = dependencies.getMessageSender();
164 final var address = addressResolver.resolveSignalServiceAddress(recipientId);
165 logger.debug("Sending retry receipt for {} to {}, device: {}",
166 errorMessage.getTimestamp(),
167 recipientId,
168 errorMessage.getDeviceId());
169 try {
170 messageSender.sendRetryReceipt(address,
171 unidentifiedAccessHelper.getAccessFor(recipientId),
172 groupId.transform(GroupId::serialize),
173 errorMessage);
174 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
175 throw new UntrustedIdentityException(account.getRecipientStore().resolveRecipientAddress(recipientId));
176 }
177 }
178
179 public SendMessageResult sendNullMessage(RecipientId recipientId) throws IOException {
180 var messageSender = dependencies.getMessageSender();
181
182 final var address = addressResolver.resolveSignalServiceAddress(recipientId);
183 try {
184 try {
185 return messageSender.sendNullMessage(address, unidentifiedAccessHelper.getAccessFor(recipientId));
186 } catch (UnregisteredUserException e) {
187 final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId);
188 final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId);
189 return messageSender.sendNullMessage(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId));
190 }
191 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
192 return SendMessageResult.identityFailure(address, e.getIdentityKey());
193 }
194 }
195
196 public SendMessageResult sendSelfMessage(
197 SignalServiceDataMessage.Builder messageBuilder
198 ) throws IOException {
199 final var recipientId = account.getSelfRecipientId();
200 final var contact = account.getContactStore().getContact(recipientId);
201 final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
202 messageBuilder.withExpiration(expirationTime);
203
204 var message = messageBuilder.build();
205 return sendSelfMessage(message);
206 }
207
208 public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message) throws IOException {
209 var messageSender = dependencies.getMessageSender();
210 try {
211 return messageSender.sendSyncMessage(message, unidentifiedAccessHelper.getAccessForSync());
212 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
213 var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId());
214 return SendMessageResult.identityFailure(address, e.getIdentityKey());
215 }
216 }
217
218 public void sendTypingMessage(
219 SignalServiceTypingMessage message, RecipientId recipientId
220 ) throws IOException, UntrustedIdentityException {
221 var messageSender = dependencies.getMessageSender();
222 final var address = addressResolver.resolveSignalServiceAddress(recipientId);
223 try {
224 try {
225 messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message);
226 } catch (UnregisteredUserException e) {
227 final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId);
228 final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId);
229 messageSender.sendTyping(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId), message);
230 }
231 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
232 throw new UntrustedIdentityException(account.getRecipientStore().resolveRecipientAddress(recipientId));
233 }
234 }
235
236 public void 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 messageSender = dependencies.getMessageSender();
244 final var recipientIdList = new ArrayList<>(g.getMembersWithout(account.getSelfRecipientId()));
245 final var addresses = recipientIdList.stream()
246 .map(addressResolver::resolveSignalServiceAddress)
247 .collect(Collectors.toList());
248 messageSender.sendTyping(addresses, unidentifiedAccessHelper.getAccessFor(recipientIdList), message, null);
249 }
250
251 private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
252 var g = groupProvider.getGroup(groupId);
253 if (g == null) {
254 throw new GroupNotFoundException(groupId);
255 }
256 if (!g.isMember(account.getSelfRecipientId())) {
257 throw new NotAGroupMemberException(groupId, g.getTitle());
258 }
259 return g;
260 }
261
262 private List<SendMessageResult> sendGroupMessageInternal(
263 final SignalServiceDataMessage message, final Set<RecipientId> recipientIds
264 ) throws IOException {
265 try {
266 var messageSender = dependencies.getMessageSender();
267 // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false.
268 final var isRecipientUpdate = false;
269 final var recipientIdList = new ArrayList<>(recipientIds);
270 final var addresses = recipientIdList.stream()
271 .map(addressResolver::resolveSignalServiceAddress)
272 .collect(Collectors.toList());
273 return messageSender.sendDataMessage(addresses,
274 unidentifiedAccessHelper.getAccessFor(recipientIdList),
275 isRecipientUpdate,
276 ContentHint.DEFAULT,
277 message,
278 SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
279 sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()),
280 () -> false);
281 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
282 return List.of();
283 }
284 }
285
286 private SendMessageResult sendMessage(
287 SignalServiceDataMessage message, RecipientId recipientId
288 ) throws IOException {
289 var messageSender = dependencies.getMessageSender();
290
291 final var address = addressResolver.resolveSignalServiceAddress(recipientId);
292 try {
293 try {
294 return messageSender.sendDataMessage(address,
295 unidentifiedAccessHelper.getAccessFor(recipientId),
296 ContentHint.DEFAULT,
297 message,
298 SignalServiceMessageSender.IndividualSendEvents.EMPTY);
299 } catch (UnregisteredUserException e) {
300 final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId);
301 return messageSender.sendDataMessage(addressResolver.resolveSignalServiceAddress(newRecipientId),
302 unidentifiedAccessHelper.getAccessFor(newRecipientId),
303 ContentHint.DEFAULT,
304 message,
305 SignalServiceMessageSender.IndividualSendEvents.EMPTY);
306 }
307 } catch (ProofRequiredException e) {
308 return SendMessageResult.proofRequiredFailure(address, e);
309 } catch (RateLimitException e) {
310 logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage());
311 return SendMessageResult.networkFailure(address);
312 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
313 return SendMessageResult.identityFailure(address, e.getIdentityKey());
314 }
315 }
316
317 private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException {
318 var address = account.getSelfAddress();
319 var transcript = new SentTranscriptMessage(Optional.of(address),
320 message.getTimestamp(),
321 message,
322 message.getExpiresInSeconds(),
323 Map.of(address, true),
324 false);
325 var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
326
327 return sendSyncMessage(syncMessage);
328 }
329
330 private void handlePossibleIdentityFailure(final SendMessageResult r) {
331 if (r.getIdentityFailure() != null) {
332 final var recipientId = recipientResolver.resolveRecipient(r.getAddress());
333 identityFailureHandler.handleIdentityFailure(recipientId, r.getIdentityFailure());
334 }
335 }
336 }