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