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