]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
Refactor message send methods
[signal-cli] / src / main / java / org / asamk / signal / dbus / DbusSignalImpl.java
1 package org.asamk.signal.dbus;
2
3 import org.asamk.Signal;
4 import org.asamk.signal.BaseConfig;
5 import org.asamk.signal.manager.AttachmentInvalidException;
6 import org.asamk.signal.manager.Manager;
7 import org.asamk.signal.manager.NotMasterDeviceException;
8 import org.asamk.signal.manager.api.Message;
9 import org.asamk.signal.manager.groups.GroupId;
10 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
11 import org.asamk.signal.manager.groups.GroupNotFoundException;
12 import org.asamk.signal.manager.groups.LastGroupAdminException;
13 import org.asamk.signal.manager.groups.NotAGroupMemberException;
14 import org.asamk.signal.manager.storage.identities.IdentityInfo;
15 import org.asamk.signal.util.ErrorUtils;
16 import org.asamk.signal.util.Util;
17 import org.freedesktop.dbus.exceptions.DBusExecutionException;
18 import org.whispersystems.libsignal.util.Pair;
19 import org.whispersystems.libsignal.util.guava.Optional;
20 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
21 import org.whispersystems.signalservice.api.messages.SendMessageResult;
22 import org.whispersystems.signalservice.api.util.InvalidNumberException;
23
24 import java.io.File;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.List;
28 import java.util.Objects;
29 import java.util.Set;
30 import java.util.stream.Collectors;
31 import java.util.stream.Stream;
32
33 import static org.asamk.signal.util.Util.getLegacyIdentifier;
34
35 public class DbusSignalImpl implements Signal {
36
37 private final Manager m;
38 private final String objectPath;
39
40 public DbusSignalImpl(final Manager m, final String objectPath) {
41 this.m = m;
42 this.objectPath = objectPath;
43 }
44
45 @Override
46 public boolean isRemote() {
47 return false;
48 }
49
50 @Override
51 public String getObjectPath() {
52 return objectPath;
53 }
54
55 @Override
56 public long sendMessage(final String message, final List<String> attachments, final String recipient) {
57 var recipients = new ArrayList<String>(1);
58 recipients.add(recipient);
59 return sendMessage(message, attachments, recipients);
60 }
61
62 private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException {
63 var error = ErrorUtils.getErrorMessageFromSendMessageResult(result);
64
65 if (error == null) {
66 return;
67 }
68
69 final var message = timestamp + "\nFailed to send message:\n" + error + '\n';
70
71 if (result.getIdentityFailure() != null) {
72 throw new Error.UntrustedIdentity(message);
73 } else {
74 throw new Error.Failure(message);
75 }
76 }
77
78 private static void checkSendMessageResults(
79 long timestamp, List<SendMessageResult> results
80 ) throws DBusExecutionException {
81 if (results.size() == 1) {
82 checkSendMessageResult(timestamp, results.get(0));
83 return;
84 }
85
86 var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
87 if (errors.size() == 0) {
88 return;
89 }
90
91 var message = new StringBuilder();
92 message.append(timestamp).append('\n');
93 message.append("Failed to send (some) messages:\n");
94 for (var error : errors) {
95 message.append(error).append('\n');
96 }
97
98 throw new Error.Failure(message.toString());
99 }
100
101 @Override
102 public long sendMessage(final String message, final List<String> attachments, final List<String> recipients) {
103 try {
104 final var results = m.sendMessage(new Message(message, attachments), recipients);
105 checkSendMessageResults(results.first(), results.second());
106 return results.first();
107 } catch (InvalidNumberException e) {
108 throw new Error.InvalidNumber(e.getMessage());
109 } catch (AttachmentInvalidException e) {
110 throw new Error.AttachmentInvalid(e.getMessage());
111 } catch (IOException e) {
112 throw new Error.Failure(e.getMessage());
113 }
114 }
115
116 @Override
117 public long sendRemoteDeleteMessage(
118 final long targetSentTimestamp, final String recipient
119 ) {
120 var recipients = new ArrayList<String>(1);
121 recipients.add(recipient);
122 return sendRemoteDeleteMessage(targetSentTimestamp, recipients);
123 }
124
125 @Override
126 public long sendRemoteDeleteMessage(
127 final long targetSentTimestamp, final List<String> recipients
128 ) {
129 try {
130 final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, recipients);
131 checkSendMessageResults(results.first(), results.second());
132 return results.first();
133 } catch (IOException e) {
134 throw new Error.Failure(e.getMessage());
135 } catch (InvalidNumberException e) {
136 throw new Error.InvalidNumber(e.getMessage());
137 }
138 }
139
140 @Override
141 public long sendGroupRemoteDeleteMessage(
142 final long targetSentTimestamp, final byte[] groupId
143 ) {
144 try {
145 final var results = m.sendGroupRemoteDeleteMessage(targetSentTimestamp, GroupId.unknownVersion(groupId));
146 checkSendMessageResults(results.first(), results.second());
147 return results.first();
148 } catch (IOException e) {
149 throw new Error.Failure(e.getMessage());
150 } catch (GroupNotFoundException | NotAGroupMemberException e) {
151 throw new Error.GroupNotFound(e.getMessage());
152 }
153 }
154
155 @Override
156 public long sendMessageReaction(
157 final String emoji,
158 final boolean remove,
159 final String targetAuthor,
160 final long targetSentTimestamp,
161 final String recipient
162 ) {
163 var recipients = new ArrayList<String>(1);
164 recipients.add(recipient);
165 return sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients);
166 }
167
168 @Override
169 public long sendMessageReaction(
170 final String emoji,
171 final boolean remove,
172 final String targetAuthor,
173 final long targetSentTimestamp,
174 final List<String> recipients
175 ) {
176 try {
177 final var results = m.sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients);
178 checkSendMessageResults(results.first(), results.second());
179 return results.first();
180 } catch (InvalidNumberException e) {
181 throw new Error.InvalidNumber(e.getMessage());
182 } catch (IOException e) {
183 throw new Error.Failure(e.getMessage());
184 }
185 }
186
187 @Override
188 public long sendNoteToSelfMessage(
189 final String message, final List<String> attachments
190 ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity {
191 try {
192 final var results = m.sendSelfMessage(new Message(message, attachments));
193 checkSendMessageResult(results.first(), results.second());
194 return results.first();
195 } catch (AttachmentInvalidException e) {
196 throw new Error.AttachmentInvalid(e.getMessage());
197 } catch (IOException e) {
198 throw new Error.Failure(e.getMessage());
199 }
200 }
201
202 @Override
203 public void sendEndSessionMessage(final List<String> recipients) {
204 try {
205 final var results = m.sendEndSessionMessage(recipients);
206 checkSendMessageResults(results.first(), results.second());
207 } catch (IOException e) {
208 throw new Error.Failure(e.getMessage());
209 } catch (InvalidNumberException e) {
210 throw new Error.InvalidNumber(e.getMessage());
211 }
212 }
213
214 @Override
215 public long sendGroupMessage(final String message, final List<String> attachments, final byte[] groupId) {
216 try {
217 var results = m.sendGroupMessage(new Message(message, attachments), GroupId.unknownVersion(groupId));
218 checkSendMessageResults(results.first(), results.second());
219 return results.first();
220 } catch (IOException e) {
221 throw new Error.Failure(e.getMessage());
222 } catch (GroupNotFoundException | NotAGroupMemberException e) {
223 throw new Error.GroupNotFound(e.getMessage());
224 } catch (AttachmentInvalidException e) {
225 throw new Error.AttachmentInvalid(e.getMessage());
226 }
227 }
228
229 @Override
230 public long sendGroupMessageReaction(
231 final String emoji,
232 final boolean remove,
233 final String targetAuthor,
234 final long targetSentTimestamp,
235 final byte[] groupId
236 ) {
237 try {
238 final var results = m.sendGroupMessageReaction(emoji,
239 remove,
240 targetAuthor,
241 targetSentTimestamp,
242 GroupId.unknownVersion(groupId));
243 checkSendMessageResults(results.first(), results.second());
244 return results.first();
245 } catch (IOException e) {
246 throw new Error.Failure(e.getMessage());
247 } catch (InvalidNumberException e) {
248 throw new Error.InvalidNumber(e.getMessage());
249 } catch (GroupNotFoundException | NotAGroupMemberException e) {
250 throw new Error.GroupNotFound(e.getMessage());
251 }
252 }
253
254 // Since contact names might be empty if not defined, also potentially return
255 // the profile name
256 @Override
257 public String getContactName(final String number) {
258 try {
259 return m.getContactOrProfileName(number);
260 } catch (InvalidNumberException e) {
261 throw new Error.InvalidNumber(e.getMessage());
262 }
263 }
264
265 @Override
266 public void setContactName(final String number, final String name) {
267 try {
268 m.setContactName(number, name);
269 } catch (InvalidNumberException e) {
270 throw new Error.InvalidNumber(e.getMessage());
271 } catch (NotMasterDeviceException e) {
272 throw new Error.Failure("This command doesn't work on linked devices.");
273 }
274 }
275
276 @Override
277 public void setContactBlocked(final String number, final boolean blocked) {
278 try {
279 m.setContactBlocked(number, blocked);
280 } catch (InvalidNumberException e) {
281 throw new Error.InvalidNumber(e.getMessage());
282 } catch (NotMasterDeviceException e) {
283 throw new Error.Failure("This command doesn't work on linked devices.");
284 }
285 }
286
287 @Override
288 public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
289 try {
290 m.setGroupBlocked(GroupId.unknownVersion(groupId), blocked);
291 } catch (GroupNotFoundException e) {
292 throw new Error.GroupNotFound(e.getMessage());
293 }
294 }
295
296 @Override
297 public List<byte[]> getGroupIds() {
298 var groups = m.getGroups();
299 var ids = new ArrayList<byte[]>(groups.size());
300 for (var group : groups) {
301 ids.add(group.getGroupId().serialize());
302 }
303 return ids;
304 }
305
306 @Override
307 public String getGroupName(final byte[] groupId) {
308 var group = m.getGroup(GroupId.unknownVersion(groupId));
309 if (group == null) {
310 return "";
311 } else {
312 return group.getTitle();
313 }
314 }
315
316 @Override
317 public List<String> getGroupMembers(final byte[] groupId) {
318 var group = m.getGroup(GroupId.unknownVersion(groupId));
319 if (group == null) {
320 return List.of();
321 } else {
322 return group.getMembers()
323 .stream()
324 .map(m::resolveSignalServiceAddress)
325 .map(Util::getLegacyIdentifier)
326 .collect(Collectors.toList());
327 }
328 }
329
330 @Override
331 public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) {
332 try {
333 if (groupId.length == 0) {
334 groupId = null;
335 }
336 if (name.isEmpty()) {
337 name = null;
338 }
339 if (members.isEmpty()) {
340 members = null;
341 }
342 if (avatar.isEmpty()) {
343 avatar = null;
344 }
345 if (groupId == null) {
346 final var results = m.createGroup(name, members, avatar == null ? null : new File(avatar));
347 checkSendMessageResults(0, results.second());
348 return results.first().serialize();
349 } else {
350 final var results = m.updateGroup(GroupId.unknownVersion(groupId),
351 name,
352 null,
353 members,
354 null,
355 null,
356 null,
357 false,
358 null,
359 null,
360 null,
361 avatar == null ? null : new File(avatar),
362 null,
363 null);
364 if (results != null) {
365 checkSendMessageResults(results.first(), results.second());
366 }
367 return groupId;
368 }
369 } catch (IOException e) {
370 throw new Error.Failure(e.getMessage());
371 } catch (GroupNotFoundException | NotAGroupMemberException e) {
372 throw new Error.GroupNotFound(e.getMessage());
373 } catch (InvalidNumberException e) {
374 throw new Error.InvalidNumber(e.getMessage());
375 } catch (AttachmentInvalidException e) {
376 throw new Error.AttachmentInvalid(e.getMessage());
377 }
378 }
379
380 @Override
381 public boolean isRegistered() {
382 return true;
383 }
384
385 @Override
386 public void updateProfile(
387 final String name,
388 final String about,
389 final String aboutEmoji,
390 String avatarPath,
391 final boolean removeAvatar
392 ) {
393 try {
394 if (avatarPath.isEmpty()) {
395 avatarPath = null;
396 }
397 Optional<File> avatarFile = removeAvatar
398 ? Optional.absent()
399 : avatarPath == null ? null : Optional.of(new File(avatarPath));
400 m.setProfile(name, null, about, aboutEmoji, avatarFile);
401 } catch (IOException e) {
402 throw new Error.Failure(e.getMessage());
403 }
404 }
405
406 // Provide option to query a version string in order to react on potential
407 // future interface changes
408 @Override
409 public String version() {
410 return BaseConfig.PROJECT_VERSION;
411 }
412
413 // Create a unique list of Numbers from Identities and Contacts to really get
414 // all numbers the system knows
415 @Override
416 public List<String> listNumbers() {
417 return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId),
418 m.getContacts().stream().map(Pair::first))
419 .map(m::resolveSignalServiceAddress)
420 .map(a -> a.getNumber().orNull())
421 .filter(Objects::nonNull)
422 .distinct()
423 .collect(Collectors.toList());
424 }
425
426 @Override
427 public List<String> getContactNumber(final String name) {
428 // Contact names have precedence.
429 var numbers = new ArrayList<String>();
430 var contacts = m.getContacts();
431 for (var c : contacts) {
432 if (name.equals(c.second().getName())) {
433 numbers.add(getLegacyIdentifier(m.resolveSignalServiceAddress(c.first())));
434 }
435 }
436 // Try profiles if no contact name was found
437 for (var identity : m.getIdentities()) {
438 final var recipientId = identity.getRecipientId();
439 final var address = m.resolveSignalServiceAddress(recipientId);
440 var number = address.getNumber().orNull();
441 if (number != null) {
442 var profile = m.getRecipientProfile(recipientId);
443 if (profile != null && profile.getDisplayName().equals(name)) {
444 numbers.add(number);
445 }
446 }
447 }
448 return numbers;
449 }
450
451 @Override
452 public void quitGroup(final byte[] groupId) {
453 var group = GroupId.unknownVersion(groupId);
454 try {
455 m.sendQuitGroupMessage(group, Set.of());
456 } catch (GroupNotFoundException | NotAGroupMemberException e) {
457 throw new Error.GroupNotFound(e.getMessage());
458 } catch (IOException | LastGroupAdminException e) {
459 throw new Error.Failure(e.getMessage());
460 } catch (InvalidNumberException e) {
461 throw new Error.InvalidNumber(e.getMessage());
462 }
463 }
464
465 @Override
466 public void joinGroup(final String groupLink) {
467 try {
468 final var linkUrl = GroupInviteLinkUrl.fromUri(groupLink);
469 if (linkUrl == null) {
470 throw new Error.Failure("Group link is invalid:");
471 }
472 m.joinGroup(linkUrl);
473 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupLinkNotActiveException e) {
474 throw new Error.Failure("Group link is invalid: " + e.getMessage());
475 } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
476 throw new Error.Failure("Group link was created with an incompatible version: " + e.getMessage());
477 } catch (IOException e) {
478 throw new Error.Failure(e.getMessage());
479 }
480 }
481
482 @Override
483 public boolean isContactBlocked(final String number) {
484 try {
485 return m.isContactBlocked(number);
486 } catch (InvalidNumberException e) {
487 throw new Error.InvalidNumber(e.getMessage());
488 }
489 }
490
491 @Override
492 public boolean isGroupBlocked(final byte[] groupId) {
493 var group = m.getGroup(GroupId.unknownVersion(groupId));
494 if (group == null) {
495 return false;
496 } else {
497 return group.isBlocked();
498 }
499 }
500
501 @Override
502 public boolean isMember(final byte[] groupId) {
503 var group = m.getGroup(GroupId.unknownVersion(groupId));
504 if (group == null) {
505 return false;
506 } else {
507 return group.isMember(m.getSelfRecipientId());
508 }
509 }
510 }