]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
Add DeviceNotFound Error
[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.Error;
5 import org.asamk.signal.BaseConfig;
6 import org.asamk.signal.commands.exceptions.IOErrorException;
7 import org.asamk.signal.manager.AttachmentInvalidException;
8 import org.asamk.signal.manager.Manager;
9 import org.asamk.signal.manager.NotMasterDeviceException;
10 import org.asamk.signal.manager.StickerPackInvalidException;
11 import org.asamk.signal.manager.UntrustedIdentityException;
12 import org.asamk.signal.manager.api.Identity;
13 import org.asamk.signal.manager.api.Message;
14 import org.asamk.signal.manager.api.RecipientIdentifier;
15 import org.asamk.signal.manager.api.TypingAction;
16 import org.asamk.signal.manager.groups.GroupId;
17 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
18 import org.asamk.signal.manager.groups.GroupNotFoundException;
19 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
20 import org.asamk.signal.manager.groups.LastGroupAdminException;
21 import org.asamk.signal.manager.groups.NotAGroupMemberException;
22 import org.asamk.signal.manager.storage.recipients.Profile;
23 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
24 import org.asamk.signal.util.ErrorUtils;
25 import org.freedesktop.dbus.DBusPath;
26 import org.freedesktop.dbus.connections.impl.DBusConnection;
27 import org.freedesktop.dbus.exceptions.DBusException;
28 import org.freedesktop.dbus.exceptions.DBusExecutionException;
29 import org.whispersystems.libsignal.InvalidKeyException;
30 import org.whispersystems.libsignal.util.Pair;
31 import org.whispersystems.libsignal.util.guava.Optional;
32 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
33 import org.whispersystems.signalservice.api.messages.SendMessageResult;
34 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
35 import org.whispersystems.signalservice.api.util.InvalidNumberException;
36 import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
37
38 import java.io.File;
39 import java.io.IOException;
40 import java.net.URI;
41 import java.net.URISyntaxException;
42 import java.util.ArrayList;
43 import java.util.Collection;
44 import java.util.HashSet;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.Objects;
48 import java.util.Set;
49 import java.util.UUID;
50 import java.util.stream.Collectors;
51 import java.util.stream.Stream;
52
53 public class DbusSignalImpl implements Signal {
54
55 private final Manager m;
56 private final DBusConnection connection;
57 private final String objectPath;
58
59 private DBusPath thisDevice;
60 private final List<StructDevice> devices = new ArrayList<>();
61
62 public DbusSignalImpl(final Manager m, DBusConnection connection, final String objectPath) {
63 this.m = m;
64 this.connection = connection;
65 this.objectPath = objectPath;
66 }
67
68 public void initObjects() {
69 updateDevices();
70 }
71
72 public void close() {
73 unExportDevices();
74 }
75
76 @Override
77 public String getObjectPath() {
78 return objectPath;
79 }
80
81 @Override
82 public String getSelfNumber() {
83 return m.getSelfNumber();
84 }
85
86 @Override
87 public void submitRateLimitChallenge(String challenge, String captchaString) throws IOErrorException {
88 final var captcha = captchaString == null ? null : captchaString.replace("signalcaptcha://", "");
89
90 try {
91 m.submitRateLimitRecaptchaChallenge(challenge, captcha);
92 } catch (IOException e) {
93 throw new IOErrorException("Submit challenge error: " + e.getMessage(), e);
94 }
95
96 }
97
98 @Override
99 public void addDevice(String uri) {
100 try {
101 m.addDeviceLink(new URI(uri));
102 } catch (IOException | InvalidKeyException e) {
103 throw new Error.Failure(e.getClass().getSimpleName() + " Add device link failed. " + e.getMessage());
104 } catch (URISyntaxException e) {
105 throw new Error.InvalidUri(e.getClass().getSimpleName()
106 + " Device link uri has invalid format: "
107 + e.getMessage());
108 }
109 }
110
111 @Override
112 public DBusPath getDevice(long deviceId) {
113 updateDevices();
114 final var deviceOptional = devices.stream().filter(g -> g.getId().equals(deviceId)).findFirst();
115 if (deviceOptional.isEmpty()) {
116 throw new Error.DeviceNotFound("Device not found");
117 }
118 return deviceOptional.get().getObjectPath();
119 }
120
121 @Override
122 public List<StructDevice> listDevices() {
123 updateDevices();
124 return this.devices;
125 }
126
127 @Override
128 public DBusPath getThisDevice() {
129 updateDevices();
130 return thisDevice;
131 }
132
133 @Override
134 public long sendMessage(final String message, final List<String> attachments, final String recipient) {
135 var recipients = new ArrayList<String>(1);
136 recipients.add(recipient);
137 return sendMessage(message, attachments, recipients);
138 }
139
140 @Override
141 public long sendMessage(final String message, final List<String> attachments, final List<String> recipients) {
142 try {
143 final var results = m.sendMessage(new Message(message, attachments),
144 getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
145 .map(RecipientIdentifier.class::cast)
146 .collect(Collectors.toSet()));
147
148 checkSendMessageResults(results.getTimestamp(), results.getResults());
149 return results.getTimestamp();
150 } catch (AttachmentInvalidException e) {
151 throw new Error.AttachmentInvalid(e.getMessage());
152 } catch (IOException e) {
153 throw new Error.Failure(e.getMessage());
154 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
155 throw new Error.GroupNotFound(e.getMessage());
156 }
157 }
158
159 @Override
160 public long sendRemoteDeleteMessage(
161 final long targetSentTimestamp, final String recipient
162 ) {
163 var recipients = new ArrayList<String>(1);
164 recipients.add(recipient);
165 return sendRemoteDeleteMessage(targetSentTimestamp, recipients);
166 }
167
168 @Override
169 public long sendRemoteDeleteMessage(
170 final long targetSentTimestamp, final List<String> recipients
171 ) {
172 try {
173 final var results = m.sendRemoteDeleteMessage(targetSentTimestamp,
174 getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
175 .map(RecipientIdentifier.class::cast)
176 .collect(Collectors.toSet()));
177 checkSendMessageResults(results.getTimestamp(), results.getResults());
178 return results.getTimestamp();
179 } catch (IOException e) {
180 throw new Error.Failure(e.getMessage());
181 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
182 throw new Error.GroupNotFound(e.getMessage());
183 }
184 }
185
186 @Override
187 public long sendGroupRemoteDeleteMessage(
188 final long targetSentTimestamp, final byte[] groupId
189 ) {
190 try {
191 final var results = m.sendRemoteDeleteMessage(targetSentTimestamp,
192 Set.of(new RecipientIdentifier.Group(getGroupId(groupId))));
193 checkSendMessageResults(results.getTimestamp(), results.getResults());
194 return results.getTimestamp();
195 } catch (IOException e) {
196 throw new Error.Failure(e.getMessage());
197 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
198 throw new Error.GroupNotFound(e.getMessage());
199 }
200 }
201
202 @Override
203 public long sendMessageReaction(
204 final String emoji,
205 final boolean remove,
206 final String targetAuthor,
207 final long targetSentTimestamp,
208 final String recipient
209 ) {
210 var recipients = new ArrayList<String>(1);
211 recipients.add(recipient);
212 return sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients);
213 }
214
215 @Override
216 public long sendMessageReaction(
217 final String emoji,
218 final boolean remove,
219 final String targetAuthor,
220 final long targetSentTimestamp,
221 final List<String> recipients
222 ) {
223 try {
224 final var results = m.sendMessageReaction(emoji,
225 remove,
226 getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()),
227 targetSentTimestamp,
228 getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
229 .map(RecipientIdentifier.class::cast)
230 .collect(Collectors.toSet()));
231 checkSendMessageResults(results.getTimestamp(), results.getResults());
232 return results.getTimestamp();
233 } catch (IOException e) {
234 throw new Error.Failure(e.getMessage());
235 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
236 throw new Error.GroupNotFound(e.getMessage());
237 }
238 }
239
240 @Override
241 public void sendTyping(
242 final String recipient, final boolean stop
243 ) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity {
244 try {
245 var recipients = new ArrayList<String>(1);
246 recipients.add(recipient);
247 m.sendTypingMessage(stop ? TypingAction.STOP : TypingAction.START,
248 getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
249 .map(RecipientIdentifier.class::cast)
250 .collect(Collectors.toSet()));
251 } catch (IOException e) {
252 throw new Error.Failure(e.getMessage());
253 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
254 throw new Error.GroupNotFound(e.getMessage());
255 } catch (UntrustedIdentityException e) {
256 throw new Error.UntrustedIdentity(e.getMessage());
257 }
258 }
259
260 @Override
261 public void sendReadReceipt(
262 final String recipient, final List<Long> messageIds
263 ) throws Error.Failure, Error.UntrustedIdentity {
264 try {
265 m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getSelfNumber()), messageIds);
266 } catch (IOException e) {
267 throw new Error.Failure(e.getMessage());
268 } catch (UntrustedIdentityException e) {
269 throw new Error.UntrustedIdentity(e.getMessage());
270 }
271 }
272
273 @Override
274 public void sendContacts() {
275 try {
276 m.sendContacts();
277 } catch (IOException e) {
278 throw new Error.Failure("SendContacts error: " + e.getMessage());
279 }
280 }
281
282 @Override
283 public void sendSyncRequest() {
284 try {
285 m.requestAllSyncData();
286 } catch (IOException e) {
287 throw new Error.Failure("Request sync data error: " + e.getMessage());
288 }
289 }
290
291 @Override
292 public long sendNoteToSelfMessage(
293 final String message, final List<String> attachments
294 ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity {
295 try {
296 final var results = m.sendMessage(new Message(message, attachments),
297 Set.of(RecipientIdentifier.NoteToSelf.INSTANCE));
298 checkSendMessageResults(results.getTimestamp(), results.getResults());
299 return results.getTimestamp();
300 } catch (AttachmentInvalidException e) {
301 throw new Error.AttachmentInvalid(e.getMessage());
302 } catch (IOException e) {
303 throw new Error.Failure(e.getMessage());
304 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
305 throw new Error.GroupNotFound(e.getMessage());
306 }
307 }
308
309 @Override
310 public void sendEndSessionMessage(final List<String> recipients) {
311 try {
312 final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber()));
313 checkSendMessageResults(results.getTimestamp(), results.getResults());
314 } catch (IOException e) {
315 throw new Error.Failure(e.getMessage());
316 }
317 }
318
319 @Override
320 public long sendGroupMessage(final String message, final List<String> attachments, final byte[] groupId) {
321 try {
322 var results = m.sendMessage(new Message(message, attachments),
323 Set.of(new RecipientIdentifier.Group(getGroupId(groupId))));
324 checkSendMessageResults(results.getTimestamp(), results.getResults());
325 return results.getTimestamp();
326 } catch (IOException e) {
327 throw new Error.Failure(e.getMessage());
328 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
329 throw new Error.GroupNotFound(e.getMessage());
330 } catch (AttachmentInvalidException e) {
331 throw new Error.AttachmentInvalid(e.getMessage());
332 }
333 }
334
335 @Override
336 public long sendGroupMessageReaction(
337 final String emoji,
338 final boolean remove,
339 final String targetAuthor,
340 final long targetSentTimestamp,
341 final byte[] groupId
342 ) {
343 try {
344 final var results = m.sendMessageReaction(emoji,
345 remove,
346 getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()),
347 targetSentTimestamp,
348 Set.of(new RecipientIdentifier.Group(getGroupId(groupId))));
349 checkSendMessageResults(results.getTimestamp(), results.getResults());
350 return results.getTimestamp();
351 } catch (IOException e) {
352 throw new Error.Failure(e.getMessage());
353 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
354 throw new Error.GroupNotFound(e.getMessage());
355 }
356 }
357
358 // Since contact names might be empty if not defined, also potentially return
359 // the profile name
360 @Override
361 public String getContactName(final String number) {
362 final var name = m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getSelfNumber()));
363 return name == null ? "" : name;
364 }
365
366 @Override
367 public void setContactName(final String number, final String name) {
368 try {
369 m.setContactName(getSingleRecipientIdentifier(number, m.getSelfNumber()), name);
370 } catch (NotMasterDeviceException e) {
371 throw new Error.Failure("This command doesn't work on linked devices.");
372 } catch (UnregisteredUserException e) {
373 throw new Error.Failure("Contact is not registered.");
374 }
375 }
376
377 @Override
378 public void setExpirationTimer(final String number, final int expiration) {
379 try {
380 m.setExpirationTimer(getSingleRecipientIdentifier(number, m.getSelfNumber()), expiration);
381 } catch (IOException e) {
382 throw new Error.Failure(e.getMessage());
383 }
384 }
385
386 @Override
387 public void setContactBlocked(final String number, final boolean blocked) {
388 try {
389 m.setContactBlocked(getSingleRecipientIdentifier(number, m.getSelfNumber()), blocked);
390 } catch (NotMasterDeviceException e) {
391 throw new Error.Failure("This command doesn't work on linked devices.");
392 } catch (IOException e) {
393 throw new Error.Failure(e.getMessage());
394 }
395 }
396
397 @Override
398 public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
399 try {
400 m.setGroupBlocked(getGroupId(groupId), blocked);
401 } catch (GroupNotFoundException e) {
402 throw new Error.GroupNotFound(e.getMessage());
403 } catch (IOException e) {
404 throw new Error.Failure(e.getMessage());
405 }
406 }
407
408 @Override
409 public List<byte[]> getGroupIds() {
410 var groups = m.getGroups();
411 var ids = new ArrayList<byte[]>(groups.size());
412 for (var group : groups) {
413 ids.add(group.getGroupId().serialize());
414 }
415 return ids;
416 }
417
418 @Override
419 public String getGroupName(final byte[] groupId) {
420 var group = m.getGroup(getGroupId(groupId));
421 if (group == null || group.getTitle() == null) {
422 return "";
423 } else {
424 return group.getTitle();
425 }
426 }
427
428 @Override
429 public List<String> getGroupMembers(final byte[] groupId) {
430 var group = m.getGroup(getGroupId(groupId));
431 if (group == null) {
432 return List.of();
433 } else {
434 return group.getMembers().stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList());
435 }
436 }
437
438 @Override
439 public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) {
440 try {
441 groupId = nullIfEmpty(groupId);
442 name = nullIfEmpty(name);
443 avatar = nullIfEmpty(avatar);
444 final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getSelfNumber());
445 if (groupId == null) {
446 final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar));
447 checkSendMessageResults(results.second().getTimestamp(), results.second().getResults());
448 return results.first().serialize();
449 } else {
450 final var results = m.updateGroup(getGroupId(groupId),
451 name,
452 null,
453 memberIdentifiers,
454 null,
455 null,
456 null,
457 false,
458 null,
459 null,
460 null,
461 avatar == null ? null : new File(avatar),
462 null,
463 null);
464 if (results != null) {
465 checkSendMessageResults(results.getTimestamp(), results.getResults());
466 }
467 return groupId;
468 }
469 } catch (IOException e) {
470 throw new Error.Failure(e.getMessage());
471 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
472 throw new Error.GroupNotFound(e.getMessage());
473 } catch (AttachmentInvalidException e) {
474 throw new Error.AttachmentInvalid(e.getMessage());
475 }
476 }
477
478 @Override
479 public boolean isRegistered() {
480 return true;
481 }
482
483 @Override
484 public boolean isRegistered(String number) {
485 var result = isRegistered(List.of(number));
486 return result.get(0);
487 }
488
489 @Override
490 public List<Boolean> isRegistered(List<String> numbers) {
491 var results = new ArrayList<Boolean>();
492 if (numbers.isEmpty()) {
493 return results;
494 }
495
496 Map<String, Pair<String, UUID>> registered;
497 try {
498 registered = m.areUsersRegistered(new HashSet<>(numbers));
499 } catch (IOException e) {
500 throw new Error.Failure(e.getMessage());
501 }
502
503 return numbers.stream().map(number -> {
504 var uuid = registered.get(number).second();
505 return uuid != null;
506 }).collect(Collectors.toList());
507 }
508
509 @Override
510 public void updateProfile(
511 String givenName,
512 String familyName,
513 String about,
514 String aboutEmoji,
515 String avatarPath,
516 final boolean removeAvatar
517 ) {
518 try {
519 givenName = nullIfEmpty(givenName);
520 familyName = nullIfEmpty(familyName);
521 about = nullIfEmpty(about);
522 aboutEmoji = nullIfEmpty(aboutEmoji);
523 avatarPath = nullIfEmpty(avatarPath);
524 Optional<File> avatarFile = removeAvatar
525 ? Optional.absent()
526 : avatarPath == null ? null : Optional.of(new File(avatarPath));
527 m.setProfile(givenName, familyName, about, aboutEmoji, avatarFile);
528 } catch (IOException e) {
529 throw new Error.Failure(e.getMessage());
530 }
531 }
532
533 @Override
534 public void updateProfile(
535 final String name,
536 final String about,
537 final String aboutEmoji,
538 String avatarPath,
539 final boolean removeAvatar
540 ) {
541 updateProfile(name, "", about, aboutEmoji, avatarPath, removeAvatar);
542 }
543
544 @Override
545 public void removePin() {
546 try {
547 m.setRegistrationLockPin(Optional.absent());
548 } catch (UnauthenticatedResponseException e) {
549 throw new Error.Failure("Remove pin failed with unauthenticated response: " + e.getMessage());
550 } catch (IOException e) {
551 throw new Error.Failure("Remove pin error: " + e.getMessage());
552 }
553 }
554
555 @Override
556 public void setPin(String registrationLockPin) {
557 try {
558 m.setRegistrationLockPin(Optional.of(registrationLockPin));
559 } catch (UnauthenticatedResponseException e) {
560 throw new Error.Failure("Set pin error failed with unauthenticated response: " + e.getMessage());
561 } catch (IOException e) {
562 throw new Error.Failure("Set pin error: " + e.getMessage());
563 }
564 }
565
566 // Provide option to query a version string in order to react on potential
567 // future interface changes
568 @Override
569 public String version() {
570 return BaseConfig.PROJECT_VERSION;
571 }
572
573 // Create a unique list of Numbers from Identities and Contacts to really get
574 // all numbers the system knows
575 @Override
576 public List<String> listNumbers() {
577 return Stream.concat(m.getIdentities().stream().map(Identity::getRecipient),
578 m.getContacts().stream().map(Pair::first))
579 .map(a -> a.getNumber().orElse(null))
580 .filter(Objects::nonNull)
581 .distinct()
582 .collect(Collectors.toList());
583 }
584
585 @Override
586 public List<String> getContactNumber(final String name) {
587 // Contact names have precedence.
588 var numbers = new ArrayList<String>();
589 var contacts = m.getContacts();
590 for (var c : contacts) {
591 if (name.equals(c.second().getName())) {
592 numbers.add(c.first().getLegacyIdentifier());
593 }
594 }
595 // Try profiles if no contact name was found
596 for (var identity : m.getIdentities()) {
597 final var address = identity.getRecipient();
598 var number = address.getNumber().orElse(null);
599 if (number != null) {
600 Profile profile = null;
601 try {
602 profile = m.getRecipientProfile(RecipientIdentifier.Single.fromAddress(address));
603 } catch (UnregisteredUserException ignored) {
604 }
605 if (profile != null && profile.getDisplayName().equals(name)) {
606 numbers.add(number);
607 }
608 }
609 }
610 return numbers;
611 }
612
613 @Override
614 public void quitGroup(final byte[] groupId) {
615 var group = getGroupId(groupId);
616 try {
617 m.quitGroup(group, Set.of());
618 } catch (GroupNotFoundException | NotAGroupMemberException e) {
619 throw new Error.GroupNotFound(e.getMessage());
620 } catch (IOException | LastGroupAdminException e) {
621 throw new Error.Failure(e.getMessage());
622 }
623 }
624
625 @Override
626 public byte[] joinGroup(final String groupLink) {
627 try {
628 final var linkUrl = GroupInviteLinkUrl.fromUri(groupLink);
629 if (linkUrl == null) {
630 throw new Error.Failure("Group link is invalid:");
631 }
632 final var result = m.joinGroup(linkUrl);
633 return result.first().serialize();
634 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupLinkNotActiveException e) {
635 throw new Error.Failure("Group link is invalid: " + e.getMessage());
636 } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
637 throw new Error.Failure("Group link was created with an incompatible version: " + e.getMessage());
638 } catch (IOException e) {
639 throw new Error.Failure(e.getMessage());
640 }
641 }
642
643 @Override
644 public boolean isContactBlocked(final String number) {
645 return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getSelfNumber()));
646 }
647
648 @Override
649 public boolean isGroupBlocked(final byte[] groupId) {
650 var group = m.getGroup(getGroupId(groupId));
651 if (group == null) {
652 return false;
653 } else {
654 return group.isBlocked();
655 }
656 }
657
658 @Override
659 public boolean isMember(final byte[] groupId) {
660 var group = m.getGroup(getGroupId(groupId));
661 if (group == null) {
662 return false;
663 } else {
664 return group.isMember();
665 }
666 }
667
668 @Override
669 public String uploadStickerPack(String stickerPackPath) {
670 File path = new File(stickerPackPath);
671 try {
672 return m.uploadStickerPack(path).toString();
673 } catch (IOException e) {
674 throw new Error.Failure("Upload error (maybe image size is too large):" + e.getMessage());
675 } catch (StickerPackInvalidException e) {
676 throw new Error.Failure("Invalid sticker pack: " + e.getMessage());
677 }
678 }
679
680 private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException {
681 var error = ErrorUtils.getErrorMessageFromSendMessageResult(result);
682
683 if (error == null) {
684 return;
685 }
686
687 final var message = timestamp + "\nFailed to send message:\n" + error + '\n';
688
689 if (result.getIdentityFailure() != null) {
690 throw new Error.UntrustedIdentity(message);
691 } else {
692 throw new Error.Failure(message);
693 }
694 }
695
696 private static void checkSendMessageResults(
697 long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results
698 ) throws DBusExecutionException {
699 final var sendMessageResults = results.values().stream().findFirst();
700 if (results.size() == 1 && sendMessageResults.get().size() == 1) {
701 checkSendMessageResult(timestamp, sendMessageResults.get().stream().findFirst().get());
702 return;
703 }
704
705 var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
706 if (errors.size() == 0) {
707 return;
708 }
709
710 var message = new StringBuilder();
711 message.append(timestamp).append('\n');
712 message.append("Failed to send (some) messages:\n");
713 for (var error : errors) {
714 message.append(error).append('\n');
715 }
716
717 throw new Error.Failure(message.toString());
718 }
719
720 private static void checkSendMessageResults(
721 long timestamp, Collection<SendMessageResult> results
722 ) throws DBusExecutionException {
723 if (results.size() == 1) {
724 checkSendMessageResult(timestamp, results.stream().findFirst().get());
725 return;
726 }
727
728 var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
729 if (errors.size() == 0) {
730 return;
731 }
732
733 var message = new StringBuilder();
734 message.append(timestamp).append('\n');
735 message.append("Failed to send (some) messages:\n");
736 for (var error : errors) {
737 message.append(error).append('\n');
738 }
739
740 throw new Error.Failure(message.toString());
741 }
742
743 private static Set<RecipientIdentifier.Single> getSingleRecipientIdentifiers(
744 final Collection<String> recipientStrings, final String localNumber
745 ) throws DBusExecutionException {
746 final var identifiers = new HashSet<RecipientIdentifier.Single>();
747 for (var recipientString : recipientStrings) {
748 identifiers.add(getSingleRecipientIdentifier(recipientString, localNumber));
749 }
750 return identifiers;
751 }
752
753 private static RecipientIdentifier.Single getSingleRecipientIdentifier(
754 final String recipientString, final String localNumber
755 ) throws DBusExecutionException {
756 try {
757 return RecipientIdentifier.Single.fromString(recipientString, localNumber);
758 } catch (InvalidNumberException e) {
759 throw new Error.InvalidNumber(e.getMessage());
760 }
761 }
762
763 private static GroupId getGroupId(byte[] groupId) throws DBusExecutionException {
764 try {
765 return GroupId.unknownVersion(groupId);
766 } catch (Throwable e) {
767 throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage());
768 }
769 }
770
771 private byte[] nullIfEmpty(final byte[] array) {
772 return array.length == 0 ? null : array;
773 }
774
775 private String nullIfEmpty(final String name) {
776 return name.isEmpty() ? null : name;
777 }
778
779 private String emptyIfNull(final String string) {
780 return string == null ? "" : string;
781 }
782
783 private static String getDeviceObjectPath(String basePath, long deviceId) {
784 return basePath + "/Devices/" + deviceId;
785 }
786
787 private void updateDevices() {
788 List<org.asamk.signal.manager.api.Device> linkedDevices;
789 try {
790 linkedDevices = m.getLinkedDevices();
791 } catch (IOException e) {
792 throw new Error.Failure("Failed to get linked devices: " + e.getMessage());
793 }
794
795 unExportDevices();
796
797 linkedDevices.forEach(d -> {
798 final var object = new DbusSignalDeviceImpl(d);
799 final var deviceObjectPath = object.getObjectPath();
800 try {
801 connection.exportObject(object);
802 } catch (DBusException e) {
803 e.printStackTrace();
804 }
805 if (d.isThisDevice()) {
806 thisDevice = new DBusPath(deviceObjectPath);
807 }
808 this.devices.add(new StructDevice(new DBusPath(deviceObjectPath), d.getId(), emptyIfNull(d.getName())));
809 });
810 }
811
812 private void unExportDevices() {
813 this.devices.stream()
814 .map(StructDevice::getObjectPath)
815 .map(DBusPath::getPath)
816 .forEach(connection::unExportObject);
817 this.devices.clear();
818 }
819
820 public class DbusSignalDeviceImpl extends DbusProperties implements Signal.Device {
821
822 private final org.asamk.signal.manager.api.Device device;
823
824 public DbusSignalDeviceImpl(final org.asamk.signal.manager.api.Device device) {
825 super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Device",
826 List.of(new DbusProperty<>("Id", device::getId),
827 new DbusProperty<>("Name", () -> emptyIfNull(device.getName()), this::setDeviceName),
828 new DbusProperty<>("Created", device::getCreated),
829 new DbusProperty<>("LastSeen", device::getLastSeen))));
830 this.device = device;
831 }
832
833 @Override
834 public String getObjectPath() {
835 return getDeviceObjectPath(objectPath, device.getId());
836 }
837
838 @Override
839 public void removeDevice() throws Error.Failure {
840 try {
841 m.removeLinkedDevices(device.getId());
842 updateDevices();
843 } catch (IOException e) {
844 throw new Error.Failure(e.getMessage());
845 }
846 }
847
848 private void setDeviceName(String name) {
849 if (!device.isThisDevice()) {
850 throw new Error.Failure("Only the name of this device can be changed");
851 }
852 try {
853 m.updateAccountAttributes(name);
854 // update device list
855 updateDevices();
856 } catch (IOException e) {
857 throw new Error.Failure(e.getMessage());
858 }
859 }
860 }
861 }