]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
implement Dbus setExpirationTimer (#735)
[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.StickerPackInvalidException;
9 import org.asamk.signal.manager.UntrustedIdentityException;
10 import org.asamk.signal.manager.api.Message;
11 import org.asamk.signal.manager.api.RecipientIdentifier;
12 import org.asamk.signal.manager.api.TypingAction;
13 import org.asamk.signal.manager.groups.GroupId;
14 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
15 import org.asamk.signal.manager.groups.GroupNotFoundException;
16 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
17 import org.asamk.signal.manager.groups.LastGroupAdminException;
18 import org.asamk.signal.manager.groups.NotAGroupMemberException;
19 import org.asamk.signal.manager.storage.identities.IdentityInfo;
20 import org.asamk.signal.util.ErrorUtils;
21 import org.asamk.signal.util.Util;
22 import org.freedesktop.dbus.exceptions.DBusExecutionException;
23 import org.whispersystems.libsignal.util.Pair;
24 import org.whispersystems.libsignal.util.guava.Optional;
25 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
26 import org.whispersystems.signalservice.api.messages.SendMessageResult;
27 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
28 import org.whispersystems.signalservice.api.util.InvalidNumberException;
29 import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
30
31 import java.io.File;
32 import java.io.IOException;
33 import java.util.ArrayList;
34 import java.util.Collection;
35 import java.util.HashSet;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Objects;
39 import java.util.Set;
40 import java.util.stream.Collectors;
41 import java.util.stream.Stream;
42
43 import static org.asamk.signal.util.Util.getLegacyIdentifier;
44
45 public class DbusSignalImpl implements Signal {
46
47 private final Manager m;
48 private final String objectPath;
49
50 public DbusSignalImpl(final Manager m, final String objectPath) {
51 this.m = m;
52 this.objectPath = objectPath;
53 }
54
55 @Override
56 public boolean isRemote() {
57 return false;
58 }
59
60 @Override
61 public String getObjectPath() {
62 return objectPath;
63 }
64
65 @Override
66 public long sendMessage(final String message, final List<String> attachments, final String recipient) {
67 var recipients = new ArrayList<String>(1);
68 recipients.add(recipient);
69 return sendMessage(message, attachments, recipients);
70 }
71
72 @Override
73 public long sendMessage(final String message, final List<String> attachments, final List<String> recipients) {
74 try {
75 final var results = m.sendMessage(new Message(message, attachments),
76 getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
77 .map(RecipientIdentifier.class::cast)
78 .collect(Collectors.toSet()));
79
80 checkSendMessageResults(results.getTimestamp(), results.getResults());
81 return results.getTimestamp();
82 } catch (AttachmentInvalidException e) {
83 throw new Error.AttachmentInvalid(e.getMessage());
84 } catch (IOException e) {
85 throw new Error.Failure(e.getMessage());
86 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
87 throw new Error.GroupNotFound(e.getMessage());
88 }
89 }
90
91 @Override
92 public long sendRemoteDeleteMessage(
93 final long targetSentTimestamp, final String recipient
94 ) {
95 var recipients = new ArrayList<String>(1);
96 recipients.add(recipient);
97 return sendRemoteDeleteMessage(targetSentTimestamp, recipients);
98 }
99
100 @Override
101 public long sendRemoteDeleteMessage(
102 final long targetSentTimestamp, final List<String> recipients
103 ) {
104 try {
105 final var results = m.sendRemoteDeleteMessage(targetSentTimestamp,
106 getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
107 .map(RecipientIdentifier.class::cast)
108 .collect(Collectors.toSet()));
109 checkSendMessageResults(results.getTimestamp(), results.getResults());
110 return results.getTimestamp();
111 } catch (IOException e) {
112 throw new Error.Failure(e.getMessage());
113 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
114 throw new Error.GroupNotFound(e.getMessage());
115 }
116 }
117
118 @Override
119 public long sendGroupRemoteDeleteMessage(
120 final long targetSentTimestamp, final byte[] groupId
121 ) {
122 try {
123 final var results = m.sendRemoteDeleteMessage(targetSentTimestamp,
124 Set.of(new RecipientIdentifier.Group(getGroupId(groupId))));
125 checkSendMessageResults(results.getTimestamp(), results.getResults());
126 return results.getTimestamp();
127 } catch (IOException e) {
128 throw new Error.Failure(e.getMessage());
129 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
130 throw new Error.GroupNotFound(e.getMessage());
131 }
132 }
133
134 @Override
135 public long sendMessageReaction(
136 final String emoji,
137 final boolean remove,
138 final String targetAuthor,
139 final long targetSentTimestamp,
140 final String recipient
141 ) {
142 var recipients = new ArrayList<String>(1);
143 recipients.add(recipient);
144 return sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients);
145 }
146
147 @Override
148 public long sendMessageReaction(
149 final String emoji,
150 final boolean remove,
151 final String targetAuthor,
152 final long targetSentTimestamp,
153 final List<String> recipients
154 ) {
155 try {
156 final var results = m.sendMessageReaction(emoji,
157 remove,
158 getSingleRecipientIdentifier(targetAuthor, m.getUsername()),
159 targetSentTimestamp,
160 getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
161 .map(RecipientIdentifier.class::cast)
162 .collect(Collectors.toSet()));
163 checkSendMessageResults(results.getTimestamp(), results.getResults());
164 return results.getTimestamp();
165 } catch (IOException e) {
166 throw new Error.Failure(e.getMessage());
167 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
168 throw new Error.GroupNotFound(e.getMessage());
169 }
170 }
171
172 @Override
173 public void sendTyping(
174 final String recipient, final boolean stop
175 ) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity {
176 try {
177 var recipients = new ArrayList<String>(1);
178 recipients.add(recipient);
179 m.sendTypingMessage(stop ? TypingAction.STOP : TypingAction.START,
180 getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
181 .map(RecipientIdentifier.class::cast)
182 .collect(Collectors.toSet()));
183 } catch (IOException e) {
184 throw new Error.Failure(e.getMessage());
185 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
186 throw new Error.GroupNotFound(e.getMessage());
187 } catch (UntrustedIdentityException e) {
188 throw new Error.UntrustedIdentity(e.getMessage());
189 }
190 }
191
192 @Override
193 public void sendReadReceipt(
194 final String recipient, final List<Long> timestamps
195 ) throws Error.Failure, Error.UntrustedIdentity {
196 try {
197 m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getUsername()), timestamps);
198 } catch (IOException e) {
199 throw new Error.Failure(e.getMessage());
200 } catch (UntrustedIdentityException e) {
201 throw new Error.UntrustedIdentity(e.getMessage());
202 }
203 }
204
205 @Override
206 public void sendContacts() {
207 try {
208 m.sendContacts();
209 } catch (IOException e) {
210 throw new Error.Failure("SendContacts error: " + e.getMessage());
211 }
212 }
213
214 @Override
215 public void sendSyncRequest() {
216 try {
217 m.requestAllSyncData();
218 } catch (IOException e) {
219 throw new Error.Failure("Request sync data error: " + e.getMessage());
220 }
221 }
222
223 @Override
224 public long sendNoteToSelfMessage(
225 final String message, final List<String> attachments
226 ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity {
227 try {
228 final var results = m.sendMessage(new Message(message, attachments),
229 Set.of(new RecipientIdentifier.NoteToSelf()));
230 checkSendMessageResults(results.getTimestamp(), results.getResults());
231 return results.getTimestamp();
232 } catch (AttachmentInvalidException e) {
233 throw new Error.AttachmentInvalid(e.getMessage());
234 } catch (IOException e) {
235 throw new Error.Failure(e.getMessage());
236 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
237 throw new Error.GroupNotFound(e.getMessage());
238 }
239 }
240
241 @Override
242 public void sendEndSessionMessage(final List<String> recipients) {
243 try {
244 final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getUsername()));
245 checkSendMessageResults(results.getTimestamp(), results.getResults());
246 } catch (IOException e) {
247 throw new Error.Failure(e.getMessage());
248 }
249 }
250
251 @Override
252 public long sendGroupMessage(final String message, final List<String> attachments, final byte[] groupId) {
253 try {
254 var results = m.sendMessage(new Message(message, attachments),
255 Set.of(new RecipientIdentifier.Group(getGroupId(groupId))));
256 checkSendMessageResults(results.getTimestamp(), results.getResults());
257 return results.getTimestamp();
258 } catch (IOException e) {
259 throw new Error.Failure(e.getMessage());
260 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
261 throw new Error.GroupNotFound(e.getMessage());
262 } catch (AttachmentInvalidException e) {
263 throw new Error.AttachmentInvalid(e.getMessage());
264 }
265 }
266
267 @Override
268 public long sendGroupMessageReaction(
269 final String emoji,
270 final boolean remove,
271 final String targetAuthor,
272 final long targetSentTimestamp,
273 final byte[] groupId
274 ) {
275 try {
276 final var results = m.sendMessageReaction(emoji,
277 remove,
278 getSingleRecipientIdentifier(targetAuthor, m.getUsername()),
279 targetSentTimestamp,
280 Set.of(new RecipientIdentifier.Group(getGroupId(groupId))));
281 checkSendMessageResults(results.getTimestamp(), results.getResults());
282 return results.getTimestamp();
283 } catch (IOException e) {
284 throw new Error.Failure(e.getMessage());
285 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
286 throw new Error.GroupNotFound(e.getMessage());
287 }
288 }
289
290 // Since contact names might be empty if not defined, also potentially return
291 // the profile name
292 @Override
293 public String getContactName(final String number) {
294 return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getUsername()));
295 }
296
297 @Override
298 public void setContactName(final String number, final String name) {
299 try {
300 m.setContactName(getSingleRecipientIdentifier(number, m.getUsername()), name);
301 } catch (NotMasterDeviceException e) {
302 throw new Error.Failure("This command doesn't work on linked devices.");
303 } catch (UnregisteredUserException e) {
304 throw new Error.Failure("Contact is not registered.");
305 }
306 }
307
308 @Override
309 public void setExpirationTimer(final String number, final int expiration) {
310 try {
311 m.setExpirationTimer(getSingleRecipientIdentifier(number, m.getUsername()), expiration);
312 } catch (IOException e) {
313 throw new Error.Failure(e.getMessage());
314 }
315 }
316
317 @Override
318 public void setContactBlocked(final String number, final boolean blocked) {
319 try {
320 m.setContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()), blocked);
321 } catch (NotMasterDeviceException e) {
322 throw new Error.Failure("This command doesn't work on linked devices.");
323 } catch (IOException e) {
324 throw new Error.Failure(e.getMessage());
325 }
326 }
327
328 @Override
329 public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
330 try {
331 m.setGroupBlocked(getGroupId(groupId), blocked);
332 } catch (GroupNotFoundException e) {
333 throw new Error.GroupNotFound(e.getMessage());
334 } catch (IOException e) {
335 throw new Error.Failure(e.getMessage());
336 }
337 }
338
339 @Override
340 public List<byte[]> getGroupIds() {
341 var groups = m.getGroups();
342 var ids = new ArrayList<byte[]>(groups.size());
343 for (var group : groups) {
344 ids.add(group.getGroupId().serialize());
345 }
346 return ids;
347 }
348
349 @Override
350 public String getGroupName(final byte[] groupId) {
351 var group = m.getGroup(getGroupId(groupId));
352 if (group == null) {
353 return "";
354 } else {
355 return group.getTitle();
356 }
357 }
358
359 @Override
360 public List<String> getGroupMembers(final byte[] groupId) {
361 var group = m.getGroup(getGroupId(groupId));
362 if (group == null) {
363 return List.of();
364 } else {
365 return group.getMembers()
366 .stream()
367 .map(m::resolveSignalServiceAddress)
368 .map(Util::getLegacyIdentifier)
369 .collect(Collectors.toList());
370 }
371 }
372
373 @Override
374 public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) {
375 try {
376 if (groupId.length == 0) {
377 groupId = null;
378 }
379 if (name.isEmpty()) {
380 name = null;
381 }
382 if (avatar.isEmpty()) {
383 avatar = null;
384 }
385 final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getUsername());
386 if (groupId == null) {
387 final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar));
388 checkSendMessageResults(results.second().getTimestamp(), results.second().getResults());
389 return results.first().serialize();
390 } else {
391 final var results = m.updateGroup(getGroupId(groupId),
392 name,
393 null,
394 memberIdentifiers,
395 null,
396 null,
397 null,
398 false,
399 null,
400 null,
401 null,
402 avatar == null ? null : new File(avatar),
403 null,
404 null);
405 if (results != null) {
406 checkSendMessageResults(results.getTimestamp(), results.getResults());
407 }
408 return groupId;
409 }
410 } catch (IOException e) {
411 throw new Error.Failure(e.getMessage());
412 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
413 throw new Error.GroupNotFound(e.getMessage());
414 } catch (AttachmentInvalidException e) {
415 throw new Error.AttachmentInvalid(e.getMessage());
416 }
417 }
418
419 @Override
420 public boolean isRegistered() {
421 return true;
422 }
423
424 @Override
425 public void updateProfile(
426 final String name,
427 final String about,
428 final String aboutEmoji,
429 String avatarPath,
430 final boolean removeAvatar
431 ) {
432 try {
433 if (avatarPath.isEmpty()) {
434 avatarPath = null;
435 }
436 Optional<File> avatarFile = removeAvatar
437 ? Optional.absent()
438 : avatarPath == null ? null : Optional.of(new File(avatarPath));
439 m.setProfile(name, null, about, aboutEmoji, avatarFile);
440 } catch (IOException e) {
441 throw new Error.Failure(e.getMessage());
442 }
443 }
444
445 @Override
446 public void removePin() {
447 try {
448 m.setRegistrationLockPin(Optional.absent());
449 } catch (UnauthenticatedResponseException e) {
450 throw new Error.Failure("Remove pin failed with unauthenticated response: " + e.getMessage());
451 } catch (IOException e) {
452 throw new Error.Failure("Remove pin error: " + e.getMessage());
453 }
454 }
455
456 @Override
457 public void setPin(String registrationLockPin) {
458 try {
459 m.setRegistrationLockPin(Optional.of(registrationLockPin));
460 } catch (UnauthenticatedResponseException e) {
461 throw new Error.Failure("Set pin error failed with unauthenticated response: " + e.getMessage());
462 } catch (IOException e) {
463 throw new Error.Failure("Set pin error: " + e.getMessage());
464 }
465 }
466
467 // Provide option to query a version string in order to react on potential
468 // future interface changes
469 @Override
470 public String version() {
471 return BaseConfig.PROJECT_VERSION;
472 }
473
474 // Create a unique list of Numbers from Identities and Contacts to really get
475 // all numbers the system knows
476 @Override
477 public List<String> listNumbers() {
478 return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId),
479 m.getContacts().stream().map(Pair::first))
480 .map(m::resolveSignalServiceAddress)
481 .map(a -> a.getNumber().orNull())
482 .filter(Objects::nonNull)
483 .distinct()
484 .collect(Collectors.toList());
485 }
486
487 @Override
488 public List<String> getContactNumber(final String name) {
489 // Contact names have precedence.
490 var numbers = new ArrayList<String>();
491 var contacts = m.getContacts();
492 for (var c : contacts) {
493 if (name.equals(c.second().getName())) {
494 numbers.add(getLegacyIdentifier(m.resolveSignalServiceAddress(c.first())));
495 }
496 }
497 // Try profiles if no contact name was found
498 for (var identity : m.getIdentities()) {
499 final var recipientId = identity.getRecipientId();
500 final var address = m.resolveSignalServiceAddress(recipientId);
501 var number = address.getNumber().orNull();
502 if (number != null) {
503 var profile = m.getRecipientProfile(recipientId);
504 if (profile != null && profile.getDisplayName().equals(name)) {
505 numbers.add(number);
506 }
507 }
508 }
509 return numbers;
510 }
511
512 @Override
513 public void quitGroup(final byte[] groupId) {
514 var group = getGroupId(groupId);
515 try {
516 m.quitGroup(group, Set.of());
517 } catch (GroupNotFoundException | NotAGroupMemberException e) {
518 throw new Error.GroupNotFound(e.getMessage());
519 } catch (IOException | LastGroupAdminException e) {
520 throw new Error.Failure(e.getMessage());
521 }
522 }
523
524 @Override
525 public byte[] joinGroup(final String groupLink) {
526 try {
527 final var linkUrl = GroupInviteLinkUrl.fromUri(groupLink);
528 if (linkUrl == null) {
529 throw new Error.Failure("Group link is invalid:");
530 }
531 final var result = m.joinGroup(linkUrl);
532 return result.first().serialize();
533 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupLinkNotActiveException e) {
534 throw new Error.Failure("Group link is invalid: " + e.getMessage());
535 } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
536 throw new Error.Failure("Group link was created with an incompatible version: " + e.getMessage());
537 } catch (IOException e) {
538 throw new Error.Failure(e.getMessage());
539 }
540 }
541
542 @Override
543 public boolean isContactBlocked(final String number) {
544 return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()));
545 }
546
547 @Override
548 public boolean isGroupBlocked(final byte[] groupId) {
549 var group = m.getGroup(getGroupId(groupId));
550 if (group == null) {
551 return false;
552 } else {
553 return group.isBlocked();
554 }
555 }
556
557 @Override
558 public boolean isMember(final byte[] groupId) {
559 var group = m.getGroup(getGroupId(groupId));
560 if (group == null) {
561 return false;
562 } else {
563 return group.isMember(m.getSelfRecipientId());
564 }
565 }
566
567 @Override
568 public String uploadStickerPack(String stickerPackPath) {
569 File path = new File(stickerPackPath);
570 try {
571 return m.uploadStickerPack(path).toString();
572 } catch (IOException e) {
573 throw new Error.Failure("Upload error (maybe image size is too large):" + e.getMessage());
574 } catch (StickerPackInvalidException e) {
575 throw new Error.Failure("Invalid sticker pack: " + e.getMessage());
576 }
577 }
578
579 private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException {
580 var error = ErrorUtils.getErrorMessageFromSendMessageResult(result);
581
582 if (error == null) {
583 return;
584 }
585
586 final var message = timestamp + "\nFailed to send message:\n" + error + '\n';
587
588 if (result.getIdentityFailure() != null) {
589 throw new Error.UntrustedIdentity(message);
590 } else {
591 throw new Error.Failure(message);
592 }
593 }
594
595 private static void checkSendMessageResults(
596 long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results
597 ) throws DBusExecutionException {
598 final var sendMessageResults = results.values().stream().findFirst();
599 if (results.size() == 1 && sendMessageResults.get().size() == 1) {
600 checkSendMessageResult(timestamp, sendMessageResults.get().stream().findFirst().get());
601 return;
602 }
603
604 var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
605 if (errors.size() == 0) {
606 return;
607 }
608
609 var message = new StringBuilder();
610 message.append(timestamp).append('\n');
611 message.append("Failed to send (some) messages:\n");
612 for (var error : errors) {
613 message.append(error).append('\n');
614 }
615
616 throw new Error.Failure(message.toString());
617 }
618
619 private static void checkSendMessageResults(
620 long timestamp, Collection<SendMessageResult> results
621 ) throws DBusExecutionException {
622 if (results.size() == 1) {
623 checkSendMessageResult(timestamp, results.stream().findFirst().get());
624 return;
625 }
626
627 var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
628 if (errors.size() == 0) {
629 return;
630 }
631
632 var message = new StringBuilder();
633 message.append(timestamp).append('\n');
634 message.append("Failed to send (some) messages:\n");
635 for (var error : errors) {
636 message.append(error).append('\n');
637 }
638
639 throw new Error.Failure(message.toString());
640 }
641
642 private static Set<RecipientIdentifier.Single> getSingleRecipientIdentifiers(
643 final Collection<String> recipientStrings, final String localNumber
644 ) throws DBusExecutionException {
645 final var identifiers = new HashSet<RecipientIdentifier.Single>();
646 for (var recipientString : recipientStrings) {
647 identifiers.add(getSingleRecipientIdentifier(recipientString, localNumber));
648 }
649 return identifiers;
650 }
651
652 private static RecipientIdentifier.Single getSingleRecipientIdentifier(
653 final String recipientString, final String localNumber
654 ) throws DBusExecutionException {
655 try {
656 return RecipientIdentifier.Single.fromString(recipientString, localNumber);
657 } catch (InvalidNumberException e) {
658 throw new Error.InvalidNumber(e.getMessage());
659 }
660 }
661
662 private static GroupId getGroupId(byte[] groupId) throws DBusExecutionException {
663 try {
664 return GroupId.unknownVersion(groupId);
665 } catch (Throwable e) {
666 throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage());
667 }
668 }
669 }