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