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