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