]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
implement Dbus sync methods (#737)
[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 setContactBlocked(final String number, final boolean blocked) {
310 try {
311 m.setContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()), blocked);
312 } catch (NotMasterDeviceException e) {
313 throw new Error.Failure("This command doesn't work on linked devices.");
314 } catch (IOException e) {
315 throw new Error.Failure(e.getMessage());
316 }
317 }
318
319 @Override
320 public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
321 try {
322 m.setGroupBlocked(getGroupId(groupId), blocked);
323 } catch (GroupNotFoundException e) {
324 throw new Error.GroupNotFound(e.getMessage());
325 } catch (IOException e) {
326 throw new Error.Failure(e.getMessage());
327 }
328 }
329
330 @Override
331 public List<byte[]> getGroupIds() {
332 var groups = m.getGroups();
333 var ids = new ArrayList<byte[]>(groups.size());
334 for (var group : groups) {
335 ids.add(group.getGroupId().serialize());
336 }
337 return ids;
338 }
339
340 @Override
341 public String getGroupName(final byte[] groupId) {
342 var group = m.getGroup(getGroupId(groupId));
343 if (group == null) {
344 return "";
345 } else {
346 return group.getTitle();
347 }
348 }
349
350 @Override
351 public List<String> getGroupMembers(final byte[] groupId) {
352 var group = m.getGroup(getGroupId(groupId));
353 if (group == null) {
354 return List.of();
355 } else {
356 return group.getMembers()
357 .stream()
358 .map(m::resolveSignalServiceAddress)
359 .map(Util::getLegacyIdentifier)
360 .collect(Collectors.toList());
361 }
362 }
363
364 @Override
365 public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) {
366 try {
367 if (groupId.length == 0) {
368 groupId = null;
369 }
370 if (name.isEmpty()) {
371 name = null;
372 }
373 if (avatar.isEmpty()) {
374 avatar = null;
375 }
376 final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getUsername());
377 if (groupId == null) {
378 final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar));
379 checkSendMessageResults(results.second().getTimestamp(), results.second().getResults());
380 return results.first().serialize();
381 } else {
382 final var results = m.updateGroup(getGroupId(groupId),
383 name,
384 null,
385 memberIdentifiers,
386 null,
387 null,
388 null,
389 false,
390 null,
391 null,
392 null,
393 avatar == null ? null : new File(avatar),
394 null,
395 null);
396 if (results != null) {
397 checkSendMessageResults(results.getTimestamp(), results.getResults());
398 }
399 return groupId;
400 }
401 } catch (IOException e) {
402 throw new Error.Failure(e.getMessage());
403 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
404 throw new Error.GroupNotFound(e.getMessage());
405 } catch (AttachmentInvalidException e) {
406 throw new Error.AttachmentInvalid(e.getMessage());
407 }
408 }
409
410 @Override
411 public boolean isRegistered() {
412 return true;
413 }
414
415 @Override
416 public void updateProfile(
417 final String name,
418 final String about,
419 final String aboutEmoji,
420 String avatarPath,
421 final boolean removeAvatar
422 ) {
423 try {
424 if (avatarPath.isEmpty()) {
425 avatarPath = null;
426 }
427 Optional<File> avatarFile = removeAvatar
428 ? Optional.absent()
429 : avatarPath == null ? null : Optional.of(new File(avatarPath));
430 m.setProfile(name, null, about, aboutEmoji, avatarFile);
431 } catch (IOException e) {
432 throw new Error.Failure(e.getMessage());
433 }
434 }
435
436 @Override
437 public void removePin() {
438 try {
439 m.setRegistrationLockPin(Optional.absent());
440 } catch (UnauthenticatedResponseException e) {
441 throw new Error.Failure("Remove pin failed with unauthenticated response: " + e.getMessage());
442 } catch (IOException e) {
443 throw new Error.Failure("Remove pin error: " + e.getMessage());
444 }
445 }
446
447 @Override
448 public void setPin(String registrationLockPin) {
449 try {
450 m.setRegistrationLockPin(Optional.of(registrationLockPin));
451 } catch (UnauthenticatedResponseException e) {
452 throw new Error.Failure("Set pin error failed with unauthenticated response: " + e.getMessage());
453 } catch (IOException e) {
454 throw new Error.Failure("Set pin error: " + e.getMessage());
455 }
456 }
457
458 // Provide option to query a version string in order to react on potential
459 // future interface changes
460 @Override
461 public String version() {
462 return BaseConfig.PROJECT_VERSION;
463 }
464
465 // Create a unique list of Numbers from Identities and Contacts to really get
466 // all numbers the system knows
467 @Override
468 public List<String> listNumbers() {
469 return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId),
470 m.getContacts().stream().map(Pair::first))
471 .map(m::resolveSignalServiceAddress)
472 .map(a -> a.getNumber().orNull())
473 .filter(Objects::nonNull)
474 .distinct()
475 .collect(Collectors.toList());
476 }
477
478 @Override
479 public List<String> getContactNumber(final String name) {
480 // Contact names have precedence.
481 var numbers = new ArrayList<String>();
482 var contacts = m.getContacts();
483 for (var c : contacts) {
484 if (name.equals(c.second().getName())) {
485 numbers.add(getLegacyIdentifier(m.resolveSignalServiceAddress(c.first())));
486 }
487 }
488 // Try profiles if no contact name was found
489 for (var identity : m.getIdentities()) {
490 final var recipientId = identity.getRecipientId();
491 final var address = m.resolveSignalServiceAddress(recipientId);
492 var number = address.getNumber().orNull();
493 if (number != null) {
494 var profile = m.getRecipientProfile(recipientId);
495 if (profile != null && profile.getDisplayName().equals(name)) {
496 numbers.add(number);
497 }
498 }
499 }
500 return numbers;
501 }
502
503 @Override
504 public void quitGroup(final byte[] groupId) {
505 var group = getGroupId(groupId);
506 try {
507 m.quitGroup(group, Set.of());
508 } catch (GroupNotFoundException | NotAGroupMemberException e) {
509 throw new Error.GroupNotFound(e.getMessage());
510 } catch (IOException | LastGroupAdminException e) {
511 throw new Error.Failure(e.getMessage());
512 }
513 }
514
515 @Override
516 public byte[] joinGroup(final String groupLink) {
517 try {
518 final var linkUrl = GroupInviteLinkUrl.fromUri(groupLink);
519 if (linkUrl == null) {
520 throw new Error.Failure("Group link is invalid:");
521 }
522 final var result = m.joinGroup(linkUrl);
523 return result.first().serialize();
524 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupLinkNotActiveException e) {
525 throw new Error.Failure("Group link is invalid: " + e.getMessage());
526 } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
527 throw new Error.Failure("Group link was created with an incompatible version: " + e.getMessage());
528 } catch (IOException e) {
529 throw new Error.Failure(e.getMessage());
530 }
531 }
532
533 @Override
534 public boolean isContactBlocked(final String number) {
535 return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()));
536 }
537
538 @Override
539 public boolean isGroupBlocked(final byte[] groupId) {
540 var group = m.getGroup(getGroupId(groupId));
541 if (group == null) {
542 return false;
543 } else {
544 return group.isBlocked();
545 }
546 }
547
548 @Override
549 public boolean isMember(final byte[] groupId) {
550 var group = m.getGroup(getGroupId(groupId));
551 if (group == null) {
552 return false;
553 } else {
554 return group.isMember(m.getSelfRecipientId());
555 }
556 }
557
558 @Override
559 public String uploadStickerPack(String stickerPackPath) {
560 File path = new File(stickerPackPath);
561 try {
562 return m.uploadStickerPack(path).toString();
563 } catch (IOException e) {
564 throw new Error.Failure("Upload error (maybe image size is too large):" + e.getMessage());
565 } catch (StickerPackInvalidException e) {
566 throw new Error.Failure("Invalid sticker pack: " + e.getMessage());
567 }
568 }
569
570 private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException {
571 var error = ErrorUtils.getErrorMessageFromSendMessageResult(result);
572
573 if (error == null) {
574 return;
575 }
576
577 final var message = timestamp + "\nFailed to send message:\n" + error + '\n';
578
579 if (result.getIdentityFailure() != null) {
580 throw new Error.UntrustedIdentity(message);
581 } else {
582 throw new Error.Failure(message);
583 }
584 }
585
586 private static void checkSendMessageResults(
587 long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results
588 ) throws DBusExecutionException {
589 final var sendMessageResults = results.values().stream().findFirst();
590 if (results.size() == 1 && sendMessageResults.get().size() == 1) {
591 checkSendMessageResult(timestamp, sendMessageResults.get().stream().findFirst().get());
592 return;
593 }
594
595 var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
596 if (errors.size() == 0) {
597 return;
598 }
599
600 var message = new StringBuilder();
601 message.append(timestamp).append('\n');
602 message.append("Failed to send (some) messages:\n");
603 for (var error : errors) {
604 message.append(error).append('\n');
605 }
606
607 throw new Error.Failure(message.toString());
608 }
609
610 private static void checkSendMessageResults(
611 long timestamp, Collection<SendMessageResult> results
612 ) throws DBusExecutionException {
613 if (results.size() == 1) {
614 checkSendMessageResult(timestamp, results.stream().findFirst().get());
615 return;
616 }
617
618 var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
619 if (errors.size() == 0) {
620 return;
621 }
622
623 var message = new StringBuilder();
624 message.append(timestamp).append('\n');
625 message.append("Failed to send (some) messages:\n");
626 for (var error : errors) {
627 message.append(error).append('\n');
628 }
629
630 throw new Error.Failure(message.toString());
631 }
632
633 private static Set<RecipientIdentifier.Single> getSingleRecipientIdentifiers(
634 final Collection<String> recipientStrings, final String localNumber
635 ) throws DBusExecutionException {
636 final var identifiers = new HashSet<RecipientIdentifier.Single>();
637 for (var recipientString : recipientStrings) {
638 identifiers.add(getSingleRecipientIdentifier(recipientString, localNumber));
639 }
640 return identifiers;
641 }
642
643 private static RecipientIdentifier.Single getSingleRecipientIdentifier(
644 final String recipientString, final String localNumber
645 ) throws DBusExecutionException {
646 try {
647 return RecipientIdentifier.Single.fromString(recipientString, localNumber);
648 } catch (InvalidNumberException e) {
649 throw new Error.InvalidNumber(e.getMessage());
650 }
651 }
652
653 private static GroupId getGroupId(byte[] groupId) throws DBusExecutionException {
654 try {
655 return GroupId.unknownVersion(groupId);
656 } catch (Throwable e) {
657 throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage());
658 }
659 }
660 }