]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
Make send behavior more deterministic if there are unregistered recipients
[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.DbusReceiveMessageHandler;
6 import org.asamk.signal.manager.AttachmentInvalidException;
7 import org.asamk.signal.manager.Manager;
8 import org.asamk.signal.manager.NotMasterDeviceException;
9 import org.asamk.signal.manager.StickerPackInvalidException;
10 import org.asamk.signal.manager.api.Identity;
11 import org.asamk.signal.manager.api.InactiveGroupLinkException;
12 import org.asamk.signal.manager.api.InvalidDeviceLinkException;
13 import org.asamk.signal.manager.api.InvalidNumberException;
14 import org.asamk.signal.manager.api.Message;
15 import org.asamk.signal.manager.api.Pair;
16 import org.asamk.signal.manager.api.RecipientIdentifier;
17 import org.asamk.signal.manager.api.SendMessageResult;
18 import org.asamk.signal.manager.api.TypingAction;
19 import org.asamk.signal.manager.api.UpdateGroup;
20 import org.asamk.signal.manager.groups.GroupId;
21 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
22 import org.asamk.signal.manager.groups.GroupLinkState;
23 import org.asamk.signal.manager.groups.GroupNotFoundException;
24 import org.asamk.signal.manager.groups.GroupPermission;
25 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
26 import org.asamk.signal.manager.groups.LastGroupAdminException;
27 import org.asamk.signal.manager.groups.NotAGroupMemberException;
28 import org.asamk.signal.manager.storage.recipients.Profile;
29 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
30 import org.asamk.signal.util.ErrorUtils;
31 import org.freedesktop.dbus.DBusPath;
32 import org.freedesktop.dbus.connections.impl.DBusConnection;
33 import org.freedesktop.dbus.exceptions.DBusException;
34 import org.freedesktop.dbus.exceptions.DBusExecutionException;
35 import org.freedesktop.dbus.interfaces.DBusInterface;
36 import org.freedesktop.dbus.types.Variant;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 import java.io.File;
41 import java.io.IOException;
42 import java.net.URI;
43 import java.net.URISyntaxException;
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.Base64;
47 import java.util.Collection;
48 import java.util.HashSet;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Objects;
52 import java.util.Optional;
53 import java.util.Set;
54 import java.util.UUID;
55 import java.util.stream.Collectors;
56 import java.util.stream.Stream;
57
58 public class DbusSignalImpl implements Signal {
59
60 private final Manager m;
61 private final DBusConnection connection;
62 private final String objectPath;
63 private final boolean noReceiveOnStart;
64
65 private DBusPath thisDevice;
66 private final List<StructDevice> devices = new ArrayList<>();
67 private final List<StructGroup> groups = new ArrayList<>();
68 private DbusReceiveMessageHandler dbusMessageHandler;
69 private int subscriberCount;
70
71 private final static Logger logger = LoggerFactory.getLogger(DbusSignalImpl.class);
72
73 public DbusSignalImpl(
74 final Manager m, DBusConnection connection, final String objectPath, final boolean noReceiveOnStart
75 ) {
76 this.m = m;
77 this.connection = connection;
78 this.objectPath = objectPath;
79 this.noReceiveOnStart = noReceiveOnStart;
80 }
81
82 public void initObjects() {
83 if (!noReceiveOnStart) {
84 subscribeReceive();
85 }
86
87 updateDevices();
88 updateGroups();
89 updateConfiguration();
90 }
91
92 public void close() {
93 if (dbusMessageHandler != null) {
94 m.removeReceiveHandler(dbusMessageHandler);
95 dbusMessageHandler = null;
96 }
97 unExportDevices();
98 unExportGroups();
99 unExportConfiguration();
100 }
101
102 @Override
103 public String getObjectPath() {
104 return objectPath;
105 }
106
107 @Override
108 public String getSelfNumber() {
109 return m.getSelfNumber();
110 }
111
112 @Override
113 public void subscribeReceive() {
114 if (dbusMessageHandler == null) {
115 dbusMessageHandler = new DbusReceiveMessageHandler(m, connection, objectPath);
116 m.addReceiveHandler(dbusMessageHandler);
117 }
118 subscriberCount++;
119 }
120
121 @Override
122 public void unsubscribeReceive() {
123 subscriberCount = Math.max(0, subscriberCount - 1);
124 if (subscriberCount == 0 && dbusMessageHandler != null) {
125 m.removeReceiveHandler(dbusMessageHandler);
126 dbusMessageHandler = null;
127 }
128 }
129
130 @Override
131 public void submitRateLimitChallenge(String challenge, String captcha) {
132 try {
133 m.submitRateLimitRecaptchaChallenge(challenge, captcha);
134 } catch (IOException e) {
135 throw new Error.Failure("Submit challenge error: " + e.getMessage());
136 }
137
138 }
139
140 @Override
141 public void unregister() throws Error.Failure {
142 try {
143 m.unregister();
144 } catch (IOException e) {
145 throw new Error.Failure("Failed to unregister: " + e.getMessage());
146 }
147 }
148
149 @Override
150 public void deleteAccount() throws Error.Failure {
151 try {
152 m.deleteAccount();
153 } catch (IOException e) {
154 throw new Error.Failure("Failed to delete account: " + e.getMessage());
155 }
156 }
157
158 @Override
159 public void addDevice(String uri) {
160 try {
161 m.addDeviceLink(new URI(uri));
162 } catch (IOException | InvalidDeviceLinkException e) {
163 throw new Error.Failure(e.getClass().getSimpleName() + " Add device link failed. " + e.getMessage());
164 } catch (URISyntaxException e) {
165 throw new Error.InvalidUri(e.getClass().getSimpleName()
166 + " Device link uri has invalid format: "
167 + e.getMessage());
168 }
169 }
170
171 @Override
172 public DBusPath getDevice(long deviceId) {
173 updateDevices();
174 final var deviceOptional = devices.stream().filter(g -> g.getId().equals(deviceId)).findFirst();
175 if (deviceOptional.isEmpty()) {
176 throw new Error.DeviceNotFound("Device not found");
177 }
178 return deviceOptional.get().getObjectPath();
179 }
180
181 @Override
182 public List<StructDevice> listDevices() {
183 updateDevices();
184 return this.devices;
185 }
186
187 @Override
188 public DBusPath getThisDevice() {
189 updateDevices();
190 return thisDevice;
191 }
192
193 @Override
194 public long sendMessage(final String message, final List<String> attachments, final String recipient) {
195 var recipients = new ArrayList<String>(1);
196 recipients.add(recipient);
197 return sendMessage(message, attachments, recipients);
198 }
199
200 @Override
201 public long sendMessage(final String message, final List<String> attachments, final List<String> recipients) {
202 try {
203 final var results = m.sendMessage(new Message(message, attachments, List.of()),
204 getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
205 .map(RecipientIdentifier.class::cast)
206 .collect(Collectors.toSet()));
207
208 checkSendMessageResults(results.timestamp(), results.results());
209 return results.timestamp();
210 } catch (AttachmentInvalidException e) {
211 throw new Error.AttachmentInvalid(e.getMessage());
212 } catch (IOException e) {
213 throw new Error.Failure(e);
214 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
215 throw new Error.GroupNotFound(e.getMessage());
216 }
217 }
218
219 @Override
220 public long sendRemoteDeleteMessage(
221 final long targetSentTimestamp, final String recipient
222 ) {
223 var recipients = new ArrayList<String>(1);
224 recipients.add(recipient);
225 return sendRemoteDeleteMessage(targetSentTimestamp, recipients);
226 }
227
228 @Override
229 public long sendRemoteDeleteMessage(
230 final long targetSentTimestamp, final List<String> recipients
231 ) {
232 try {
233 final var results = m.sendRemoteDeleteMessage(targetSentTimestamp,
234 getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
235 .map(RecipientIdentifier.class::cast)
236 .collect(Collectors.toSet()));
237 checkSendMessageResults(results.timestamp(), results.results());
238 return results.timestamp();
239 } catch (IOException e) {
240 throw new Error.Failure(e.getMessage());
241 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
242 throw new Error.GroupNotFound(e.getMessage());
243 }
244 }
245
246 @Override
247 public long sendGroupRemoteDeleteMessage(
248 final long targetSentTimestamp, final byte[] groupId
249 ) {
250 try {
251 final var results = m.sendRemoteDeleteMessage(targetSentTimestamp,
252 Set.of(new RecipientIdentifier.Group(getGroupId(groupId))));
253 checkSendMessageResults(results.timestamp(), results.results());
254 return results.timestamp();
255 } catch (IOException e) {
256 throw new Error.Failure(e.getMessage());
257 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
258 throw new Error.GroupNotFound(e.getMessage());
259 }
260 }
261
262 @Override
263 public long sendMessageReaction(
264 final String emoji,
265 final boolean remove,
266 final String targetAuthor,
267 final long targetSentTimestamp,
268 final String recipient
269 ) {
270 var recipients = new ArrayList<String>(1);
271 recipients.add(recipient);
272 return sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients);
273 }
274
275 @Override
276 public long sendMessageReaction(
277 final String emoji,
278 final boolean remove,
279 final String targetAuthor,
280 final long targetSentTimestamp,
281 final List<String> recipients
282 ) {
283 try {
284 final var results = m.sendMessageReaction(emoji,
285 remove,
286 getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()),
287 targetSentTimestamp,
288 getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
289 .map(RecipientIdentifier.class::cast)
290 .collect(Collectors.toSet()));
291 checkSendMessageResults(results.timestamp(), results.results());
292 return results.timestamp();
293 } catch (IOException e) {
294 throw new Error.Failure(e.getMessage());
295 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
296 throw new Error.GroupNotFound(e.getMessage());
297 }
298 }
299
300 @Override
301 public void sendTyping(
302 final String recipient, final boolean stop
303 ) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity {
304 try {
305 var recipients = new ArrayList<String>(1);
306 recipients.add(recipient);
307 final var results = m.sendTypingMessage(stop ? TypingAction.STOP : TypingAction.START,
308 getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
309 .map(RecipientIdentifier.class::cast)
310 .collect(Collectors.toSet()));
311 checkSendMessageResults(results.timestamp(), results.results());
312 } catch (IOException e) {
313 throw new Error.Failure(e.getMessage());
314 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
315 throw new Error.GroupNotFound(e.getMessage());
316 }
317 }
318
319 @Override
320 public void sendReadReceipt(
321 final String recipient, final List<Long> messageIds
322 ) throws Error.Failure, Error.UntrustedIdentity {
323 try {
324 final var results = m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getSelfNumber()),
325 messageIds);
326 checkSendMessageResults(results.timestamp(), results.results());
327 } catch (IOException e) {
328 throw new Error.Failure(e.getMessage());
329 }
330 }
331
332 @Override
333 public void sendViewedReceipt(
334 final String recipient, final List<Long> messageIds
335 ) throws Error.Failure, Error.UntrustedIdentity {
336 try {
337 final var results = m.sendViewedReceipt(getSingleRecipientIdentifier(recipient, m.getSelfNumber()),
338 messageIds);
339 checkSendMessageResults(results.timestamp(), results.results());
340 } catch (IOException e) {
341 throw new Error.Failure(e.getMessage());
342 }
343 }
344
345 @Override
346 public void sendContacts() {
347 try {
348 m.sendContacts();
349 } catch (IOException e) {
350 throw new Error.Failure("SendContacts error: " + e.getMessage());
351 }
352 }
353
354 @Override
355 public void sendSyncRequest() {
356 try {
357 m.requestAllSyncData();
358 } catch (IOException e) {
359 throw new Error.Failure("Request sync data error: " + e.getMessage());
360 }
361 }
362
363 @Override
364 public long sendNoteToSelfMessage(
365 final String message, final List<String> attachments
366 ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity {
367 try {
368 final var results = m.sendMessage(new Message(message, attachments, List.of()),
369 Set.of(RecipientIdentifier.NoteToSelf.INSTANCE));
370 checkSendMessageResults(results.timestamp(), results.results());
371 return results.timestamp();
372 } catch (AttachmentInvalidException e) {
373 throw new Error.AttachmentInvalid(e.getMessage());
374 } catch (IOException e) {
375 throw new Error.Failure(e.getMessage());
376 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
377 throw new Error.GroupNotFound(e.getMessage());
378 }
379 }
380
381 @Override
382 public void sendEndSessionMessage(final List<String> recipients) {
383 try {
384 final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber()));
385 checkSendMessageResults(results.timestamp(), results.results());
386 } catch (IOException e) {
387 throw new Error.Failure(e.getMessage());
388 }
389 }
390
391 @Override
392 public long sendGroupMessage(final String message, final List<String> attachments, final byte[] groupId) {
393 try {
394 var results = m.sendMessage(new Message(message, attachments, List.of()),
395 Set.of(new RecipientIdentifier.Group(getGroupId(groupId))));
396 checkSendMessageResults(results.timestamp(), results.results());
397 return results.timestamp();
398 } catch (IOException e) {
399 throw new Error.Failure(e.getMessage());
400 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
401 throw new Error.GroupNotFound(e.getMessage());
402 } catch (AttachmentInvalidException e) {
403 throw new Error.AttachmentInvalid(e.getMessage());
404 }
405 }
406
407 @Override
408 public long sendGroupMessageReaction(
409 final String emoji,
410 final boolean remove,
411 final String targetAuthor,
412 final long targetSentTimestamp,
413 final byte[] groupId
414 ) {
415 try {
416 final var results = m.sendMessageReaction(emoji,
417 remove,
418 getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()),
419 targetSentTimestamp,
420 Set.of(new RecipientIdentifier.Group(getGroupId(groupId))));
421 checkSendMessageResults(results.timestamp(), results.results());
422 return results.timestamp();
423 } catch (IOException e) {
424 throw new Error.Failure(e.getMessage());
425 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
426 throw new Error.GroupNotFound(e.getMessage());
427 }
428 }
429
430 // Since contact names might be empty if not defined, also potentially return
431 // the profile name
432 @Override
433 public String getContactName(final String number) {
434 final var name = m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getSelfNumber()));
435 return name == null ? "" : name;
436 }
437
438 @Override
439 public void setContactName(final String number, final String name) {
440 try {
441 m.setContactName(getSingleRecipientIdentifier(number, m.getSelfNumber()), name);
442 } catch (NotMasterDeviceException e) {
443 throw new Error.Failure("This command doesn't work on linked devices.");
444 } catch (IOException e) {
445 throw new Error.Failure("Contact is not registered.");
446 }
447 }
448
449 @Override
450 public void setExpirationTimer(final String number, final int expiration) {
451 try {
452 m.setExpirationTimer(getSingleRecipientIdentifier(number, m.getSelfNumber()), expiration);
453 } catch (IOException e) {
454 throw new Error.Failure(e.getMessage());
455 }
456 }
457
458 @Override
459 public void setContactBlocked(final String number, final boolean blocked) {
460 try {
461 m.setContactBlocked(getSingleRecipientIdentifier(number, m.getSelfNumber()), blocked);
462 } catch (NotMasterDeviceException e) {
463 throw new Error.Failure("This command doesn't work on linked devices.");
464 } catch (IOException e) {
465 throw new Error.Failure(e.getMessage());
466 }
467 }
468
469 @Override
470 public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
471 try {
472 m.setGroupBlocked(getGroupId(groupId), blocked);
473 } catch (NotMasterDeviceException e) {
474 throw new Error.Failure("This command doesn't work on linked devices.");
475 } catch (GroupNotFoundException e) {
476 throw new Error.GroupNotFound(e.getMessage());
477 } catch (IOException e) {
478 throw new Error.Failure(e.getMessage());
479 }
480 }
481
482 @Override
483 public List<byte[]> getGroupIds() {
484 var groups = m.getGroups();
485 var ids = new ArrayList<byte[]>(groups.size());
486 for (var group : groups) {
487 ids.add(group.groupId().serialize());
488 }
489 return ids;
490 }
491
492 @Override
493 public DBusPath getGroup(final byte[] groupId) {
494 updateGroups();
495 final var groupOptional = groups.stream().filter(g -> Arrays.equals(g.getId(), groupId)).findFirst();
496 if (groupOptional.isEmpty()) {
497 throw new Error.GroupNotFound("Group not found");
498 }
499 return groupOptional.get().getObjectPath();
500 }
501
502 @Override
503 public List<StructGroup> listGroups() {
504 updateGroups();
505 return groups;
506 }
507
508 @Override
509 public String getGroupName(final byte[] groupId) {
510 var group = m.getGroup(getGroupId(groupId));
511 if (group == null || group.title() == null) {
512 return "";
513 } else {
514 return group.title();
515 }
516 }
517
518 @Override
519 public List<String> getGroupMembers(final byte[] groupId) {
520 var group = m.getGroup(getGroupId(groupId));
521 if (group == null) {
522 return List.of();
523 } else {
524 final var members = group.members();
525 return getRecipientStrings(members);
526 }
527 }
528
529 @Override
530 public byte[] createGroup(
531 final String name, final List<String> members, final String avatar
532 ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber {
533 return updateGroup(new byte[0], name, members, avatar);
534 }
535
536 @Override
537 public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) {
538 try {
539 groupId = nullIfEmpty(groupId);
540 name = nullIfEmpty(name);
541 avatar = nullIfEmpty(avatar);
542 final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getSelfNumber());
543 if (groupId == null) {
544 final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar));
545 checkSendMessageResults(results.second().timestamp(), results.second().results());
546 return results.first().serialize();
547 } else {
548 final var results = m.updateGroup(getGroupId(groupId),
549 UpdateGroup.newBuilder()
550 .withName(name)
551 .withMembers(memberIdentifiers)
552 .withAvatarFile(avatar == null ? null : new File(avatar))
553 .build());
554 if (results != null) {
555 checkSendMessageResults(results.timestamp(), results.results());
556 }
557 return groupId;
558 }
559 } catch (IOException e) {
560 throw new Error.Failure(e.getMessage());
561 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
562 throw new Error.GroupNotFound(e.getMessage());
563 } catch (AttachmentInvalidException e) {
564 throw new Error.AttachmentInvalid(e.getMessage());
565 }
566 }
567
568 @Override
569 public boolean isRegistered() {
570 return true;
571 }
572
573 @Override
574 public boolean isRegistered(String number) {
575 var result = isRegistered(List.of(number));
576 return result.get(0);
577 }
578
579 @Override
580 public List<Boolean> isRegistered(List<String> numbers) {
581 var results = new ArrayList<Boolean>();
582 if (numbers.isEmpty()) {
583 return results;
584 }
585
586 Map<String, Pair<String, UUID>> registered;
587 try {
588 registered = m.areUsersRegistered(new HashSet<>(numbers));
589 } catch (IOException e) {
590 throw new Error.Failure(e.getMessage());
591 }
592
593 return numbers.stream().map(number -> {
594 var uuid = registered.get(number).second();
595 return uuid != null;
596 }).collect(Collectors.toList());
597 }
598
599 @Override
600 public void updateProfile(
601 String givenName,
602 String familyName,
603 String about,
604 String aboutEmoji,
605 String avatarPath,
606 final boolean removeAvatar
607 ) {
608 try {
609 givenName = nullIfEmpty(givenName);
610 familyName = nullIfEmpty(familyName);
611 about = nullIfEmpty(about);
612 aboutEmoji = nullIfEmpty(aboutEmoji);
613 avatarPath = nullIfEmpty(avatarPath);
614 Optional<File> avatarFile = removeAvatar
615 ? Optional.empty()
616 : avatarPath == null ? null : Optional.of(new File(avatarPath));
617 m.setProfile(givenName, familyName, about, aboutEmoji, avatarFile);
618 } catch (IOException e) {
619 throw new Error.Failure(e.getMessage());
620 }
621 }
622
623 @Override
624 public void updateProfile(
625 final String name,
626 final String about,
627 final String aboutEmoji,
628 String avatarPath,
629 final boolean removeAvatar
630 ) {
631 updateProfile(name, "", about, aboutEmoji, avatarPath, removeAvatar);
632 }
633
634 @Override
635 public void removePin() {
636 try {
637 m.setRegistrationLockPin(Optional.empty());
638 } catch (IOException e) {
639 throw new Error.Failure("Remove pin error: " + e.getMessage());
640 }
641 }
642
643 @Override
644 public void setPin(String registrationLockPin) {
645 try {
646 m.setRegistrationLockPin(Optional.of(registrationLockPin));
647 } catch (IOException e) {
648 throw new Error.Failure("Set pin error: " + e.getMessage());
649 }
650 }
651
652 // Provide option to query a version string in order to react on potential
653 // future interface changes
654 @Override
655 public String version() {
656 return BaseConfig.PROJECT_VERSION;
657 }
658
659 // Create a unique list of Numbers from Identities and Contacts to really get
660 // all numbers the system knows
661 @Override
662 public List<String> listNumbers() {
663 return Stream.concat(m.getIdentities().stream().map(Identity::recipient),
664 m.getContacts().stream().map(Pair::first))
665 .map(a -> a.getNumber().orElse(null))
666 .filter(Objects::nonNull)
667 .distinct()
668 .collect(Collectors.toList());
669 }
670
671 @Override
672 public List<String> getContactNumber(final String name) {
673 // Contact names have precedence.
674 var numbers = new ArrayList<String>();
675 var contacts = m.getContacts();
676 for (var c : contacts) {
677 if (name.equals(c.second().getName())) {
678 numbers.add(c.first().getLegacyIdentifier());
679 }
680 }
681 // Try profiles if no contact name was found
682 for (var identity : m.getIdentities()) {
683 final var address = identity.recipient();
684 var number = address.getNumber().orElse(null);
685 if (number != null) {
686 Profile profile = null;
687 try {
688 profile = m.getRecipientProfile(RecipientIdentifier.Single.fromAddress(address));
689 } catch (IOException ignored) {
690 }
691 if (profile != null && profile.getDisplayName().equals(name)) {
692 numbers.add(number);
693 }
694 }
695 }
696 return numbers;
697 }
698
699 @Override
700 public void quitGroup(final byte[] groupId) {
701 var group = getGroupId(groupId);
702 try {
703 m.quitGroup(group, Set.of());
704 } catch (GroupNotFoundException | NotAGroupMemberException e) {
705 throw new Error.GroupNotFound(e.getMessage());
706 } catch (IOException | LastGroupAdminException e) {
707 throw new Error.Failure(e.getMessage());
708 }
709 }
710
711 @Override
712 public byte[] joinGroup(final String groupLink) {
713 try {
714 final var linkUrl = GroupInviteLinkUrl.fromUri(groupLink);
715 if (linkUrl == null) {
716 throw new Error.Failure("Group link is invalid:");
717 }
718 final var result = m.joinGroup(linkUrl);
719 return result.first().serialize();
720 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | InactiveGroupLinkException e) {
721 throw new Error.Failure("Group link is invalid: " + e.getMessage());
722 } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
723 throw new Error.Failure("Group link was created with an incompatible version: " + e.getMessage());
724 } catch (IOException e) {
725 throw new Error.Failure(e.getMessage());
726 }
727 }
728
729 @Override
730 public boolean isContactBlocked(final String number) {
731 return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getSelfNumber()));
732 }
733
734 @Override
735 public boolean isGroupBlocked(final byte[] groupId) {
736 var group = m.getGroup(getGroupId(groupId));
737 if (group == null) {
738 return false;
739 } else {
740 return group.isBlocked();
741 }
742 }
743
744 @Override
745 public boolean isMember(final byte[] groupId) {
746 var group = m.getGroup(getGroupId(groupId));
747 if (group == null) {
748 return false;
749 } else {
750 return group.isMember();
751 }
752 }
753
754 @Override
755 public String uploadStickerPack(String stickerPackPath) {
756 File path = new File(stickerPackPath);
757 try {
758 return m.uploadStickerPack(path).toString();
759 } catch (IOException e) {
760 throw new Error.Failure("Upload error (maybe image size is too large):" + e.getMessage());
761 } catch (StickerPackInvalidException e) {
762 throw new Error.Failure("Invalid sticker pack: " + e.getMessage());
763 }
764 }
765
766 private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException {
767 var error = ErrorUtils.getErrorMessageFromSendMessageResult(result);
768
769 if (error == null) {
770 return;
771 }
772
773 final var message = timestamp + "\nFailed to send message:\n" + error + '\n';
774
775 if (result.isIdentityFailure()) {
776 throw new Error.UntrustedIdentity(message);
777 } else {
778 throw new Error.Failure(message);
779 }
780 }
781
782 private static void checkSendMessageResults(
783 long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results
784 ) throws DBusExecutionException {
785 final var sendMessageResults = results.values().stream().findFirst();
786 if (results.size() == 1 && sendMessageResults.get().size() == 1) {
787 checkSendMessageResult(timestamp, sendMessageResults.get().stream().findFirst().get());
788 return;
789 }
790
791 var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
792 if (errors.size() == 0) {
793 return;
794 }
795
796 var message = new StringBuilder();
797 message.append(timestamp).append('\n');
798 message.append("Failed to send (some) messages:\n");
799 for (var error : errors) {
800 message.append(error).append('\n');
801 }
802
803 throw new Error.Failure(message.toString());
804 }
805
806 private static void checkSendMessageResults(
807 long timestamp, Collection<SendMessageResult> results
808 ) throws DBusExecutionException {
809 if (results.size() == 1) {
810 checkSendMessageResult(timestamp, results.stream().findFirst().get());
811 return;
812 }
813
814 var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
815 if (errors.size() == 0) {
816 return;
817 }
818
819 var message = new StringBuilder();
820 message.append(timestamp).append('\n');
821 message.append("Failed to send (some) messages:\n");
822 for (var error : errors) {
823 message.append(error).append('\n');
824 }
825
826 throw new Error.Failure(message.toString());
827 }
828
829 private static List<String> getRecipientStrings(final Set<RecipientAddress> members) {
830 return members.stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList());
831 }
832
833 private static Set<RecipientIdentifier.Single> getSingleRecipientIdentifiers(
834 final Collection<String> recipientStrings, final String localNumber
835 ) throws DBusExecutionException {
836 final var identifiers = new HashSet<RecipientIdentifier.Single>();
837 for (var recipientString : recipientStrings) {
838 identifiers.add(getSingleRecipientIdentifier(recipientString, localNumber));
839 }
840 return identifiers;
841 }
842
843 private static RecipientIdentifier.Single getSingleRecipientIdentifier(
844 final String recipientString, final String localNumber
845 ) throws DBusExecutionException {
846 try {
847 return RecipientIdentifier.Single.fromString(recipientString, localNumber);
848 } catch (InvalidNumberException e) {
849 throw new Error.InvalidNumber(e.getMessage());
850 }
851 }
852
853 private static GroupId getGroupId(byte[] groupId) throws DBusExecutionException {
854 try {
855 return GroupId.unknownVersion(groupId);
856 } catch (Throwable e) {
857 throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage());
858 }
859 }
860
861 private byte[] nullIfEmpty(final byte[] array) {
862 return array.length == 0 ? null : array;
863 }
864
865 private String nullIfEmpty(final String name) {
866 return name.isEmpty() ? null : name;
867 }
868
869 private String emptyIfNull(final String string) {
870 return string == null ? "" : string;
871 }
872
873 private static String getDeviceObjectPath(String basePath, long deviceId) {
874 return basePath + "/Devices/" + deviceId;
875 }
876
877 private void updateDevices() {
878 List<org.asamk.signal.manager.api.Device> linkedDevices;
879 try {
880 linkedDevices = m.getLinkedDevices();
881 } catch (IOException e) {
882 throw new Error.Failure("Failed to get linked devices: " + e.getMessage());
883 }
884
885 unExportDevices();
886
887 linkedDevices.forEach(d -> {
888 final var object = new DbusSignalDeviceImpl(d);
889 final var deviceObjectPath = object.getObjectPath();
890 exportObject(object);
891 if (d.isThisDevice()) {
892 thisDevice = new DBusPath(deviceObjectPath);
893 }
894 this.devices.add(new StructDevice(new DBusPath(deviceObjectPath), d.id(), emptyIfNull(d.name())));
895 });
896 }
897
898 private void unExportDevices() {
899 this.devices.stream()
900 .map(StructDevice::getObjectPath)
901 .map(DBusPath::getPath)
902 .forEach(connection::unExportObject);
903 this.devices.clear();
904 }
905
906 private static String getGroupObjectPath(String basePath, byte[] groupId) {
907 return basePath + "/Groups/" + Base64.getEncoder()
908 .encodeToString(groupId)
909 .replace("+", "_")
910 .replace("/", "_")
911 .replace("=", "_");
912 }
913
914 private void updateGroups() {
915 List<org.asamk.signal.manager.api.Group> groups;
916 groups = m.getGroups();
917
918 unExportGroups();
919
920 groups.forEach(g -> {
921 final var object = new DbusSignalGroupImpl(g.groupId());
922 exportObject(object);
923 this.groups.add(new StructGroup(new DBusPath(object.getObjectPath()),
924 g.groupId().serialize(),
925 emptyIfNull(g.title())));
926 });
927 }
928
929 private void unExportGroups() {
930 this.groups.stream().map(StructGroup::getObjectPath).map(DBusPath::getPath).forEach(connection::unExportObject);
931 this.groups.clear();
932 }
933
934 private static String getConfigurationObjectPath(String basePath) {
935 return basePath + "/Configuration";
936 }
937
938 private void updateConfiguration() {
939 unExportConfiguration();
940 final var object = new DbusSignalConfigurationImpl();
941 exportObject(object);
942 }
943
944 private void unExportConfiguration() {
945 final var objectPath = getConfigurationObjectPath(this.objectPath);
946 connection.unExportObject(objectPath);
947 }
948
949 private void exportObject(final DBusInterface object) {
950 try {
951 connection.exportObject(object);
952 logger.debug("Exported dbus object: " + object.getObjectPath());
953 } catch (DBusException e) {
954 e.printStackTrace();
955 }
956 }
957
958 public class DbusSignalDeviceImpl extends DbusProperties implements Signal.Device {
959
960 private final org.asamk.signal.manager.api.Device device;
961
962 public DbusSignalDeviceImpl(final org.asamk.signal.manager.api.Device device) {
963 super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Device",
964 List.of(new DbusProperty<>("Id", device::id),
965 new DbusProperty<>("Name", () -> emptyIfNull(device.name()), this::setDeviceName),
966 new DbusProperty<>("Created", device::created),
967 new DbusProperty<>("LastSeen", device::lastSeen))));
968 this.device = device;
969 }
970
971 @Override
972 public String getObjectPath() {
973 return getDeviceObjectPath(objectPath, device.id());
974 }
975
976 @Override
977 public void removeDevice() throws Error.Failure {
978 try {
979 m.removeLinkedDevices(device.id());
980 updateDevices();
981 } catch (IOException e) {
982 throw new Error.Failure(e.getMessage());
983 }
984 }
985
986 private void setDeviceName(String name) {
987 if (!device.isThisDevice()) {
988 throw new Error.Failure("Only the name of this device can be changed");
989 }
990 try {
991 m.updateAccountAttributes(name);
992 // update device list
993 updateDevices();
994 } catch (IOException e) {
995 throw new Error.Failure(e.getMessage());
996 }
997 }
998 }
999
1000 public class DbusSignalConfigurationImpl extends DbusProperties implements Signal.Configuration {
1001
1002 public DbusSignalConfigurationImpl(
1003 ) {
1004 super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Configuration",
1005 List.of(new DbusProperty<>("ReadReceipts", this::getReadReceipts, this::setReadReceipts),
1006 new DbusProperty<>("UnidentifiedDeliveryIndicators",
1007 this::getUnidentifiedDeliveryIndicators,
1008 this::setUnidentifiedDeliveryIndicators),
1009 new DbusProperty<>("TypingIndicators",
1010 this::getTypingIndicators,
1011 this::setTypingIndicators),
1012 new DbusProperty<>("LinkPreviews", this::getLinkPreviews, this::setLinkPreviews))));
1013
1014 }
1015
1016 @Override
1017 public String getObjectPath() {
1018 return getConfigurationObjectPath(objectPath);
1019 }
1020
1021 public void setReadReceipts(Boolean readReceipts) {
1022 setConfiguration(readReceipts, null, null, null);
1023 }
1024
1025 public void setUnidentifiedDeliveryIndicators(Boolean unidentifiedDeliveryIndicators) {
1026 setConfiguration(null, unidentifiedDeliveryIndicators, null, null);
1027 }
1028
1029 public void setTypingIndicators(Boolean typingIndicators) {
1030 setConfiguration(null, null, typingIndicators, null);
1031 }
1032
1033 public void setLinkPreviews(Boolean linkPreviews) {
1034 setConfiguration(null, null, null, linkPreviews);
1035 }
1036
1037 private void setConfiguration(
1038 Boolean readReceipts,
1039 Boolean unidentifiedDeliveryIndicators,
1040 Boolean typingIndicators,
1041 Boolean linkPreviews
1042 ) {
1043 try {
1044 m.updateConfiguration(new org.asamk.signal.manager.api.Configuration(Optional.ofNullable(readReceipts),
1045 Optional.ofNullable(unidentifiedDeliveryIndicators),
1046 Optional.ofNullable(typingIndicators),
1047 Optional.ofNullable(linkPreviews)));
1048 } catch (IOException e) {
1049 throw new Error.Failure("UpdateAccount error: " + e.getMessage());
1050 } catch (NotMasterDeviceException e) {
1051 throw new Error.Failure("This command doesn't work on linked devices.");
1052 }
1053 }
1054
1055 private boolean getReadReceipts() {
1056 return m.getConfiguration().readReceipts().orElse(false);
1057 }
1058
1059 private boolean getUnidentifiedDeliveryIndicators() {
1060 return m.getConfiguration().unidentifiedDeliveryIndicators().orElse(false);
1061 }
1062
1063 private boolean getTypingIndicators() {
1064 return m.getConfiguration().typingIndicators().orElse(false);
1065 }
1066
1067 private boolean getLinkPreviews() {
1068 return m.getConfiguration().linkPreviews().orElse(false);
1069 }
1070 }
1071
1072 public class DbusSignalGroupImpl extends DbusProperties implements Signal.Group {
1073
1074 private final GroupId groupId;
1075
1076 public DbusSignalGroupImpl(final GroupId groupId) {
1077 this.groupId = groupId;
1078 super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Group",
1079 List.of(new DbusProperty<>("Id", groupId::serialize),
1080 new DbusProperty<>("Name", () -> emptyIfNull(getGroup().title()), this::setGroupName),
1081 new DbusProperty<>("Description",
1082 () -> emptyIfNull(getGroup().description()),
1083 this::setGroupDescription),
1084 new DbusProperty<>("Avatar", this::setGroupAvatar),
1085 new DbusProperty<>("IsBlocked", () -> getGroup().isBlocked(), this::setIsBlocked),
1086 new DbusProperty<>("IsMember", () -> getGroup().isMember()),
1087 new DbusProperty<>("IsAdmin", () -> getGroup().isAdmin()),
1088 new DbusProperty<>("MessageExpirationTimer",
1089 () -> getGroup().messageExpirationTimer(),
1090 this::setMessageExpirationTime),
1091 new DbusProperty<>("Members",
1092 () -> new Variant<>(getRecipientStrings(getGroup().members()), "as")),
1093 new DbusProperty<>("PendingMembers",
1094 () -> new Variant<>(getRecipientStrings(getGroup().pendingMembers()), "as")),
1095 new DbusProperty<>("RequestingMembers",
1096 () -> new Variant<>(getRecipientStrings(getGroup().requestingMembers()), "as")),
1097 new DbusProperty<>("Admins",
1098 () -> new Variant<>(getRecipientStrings(getGroup().adminMembers()), "as")),
1099 new DbusProperty<>("PermissionAddMember",
1100 () -> getGroup().permissionAddMember().name(),
1101 this::setGroupPermissionAddMember),
1102 new DbusProperty<>("PermissionEditDetails",
1103 () -> getGroup().permissionEditDetails().name(),
1104 this::setGroupPermissionEditDetails),
1105 new DbusProperty<>("PermissionSendMessage",
1106 () -> getGroup().permissionSendMessage().name(),
1107 this::setGroupPermissionSendMessage),
1108 new DbusProperty<>("GroupInviteLink", () -> {
1109 final var groupInviteLinkUrl = getGroup().groupInviteLinkUrl();
1110 return groupInviteLinkUrl == null ? "" : groupInviteLinkUrl.getUrl();
1111 }))));
1112 }
1113
1114 @Override
1115 public String getObjectPath() {
1116 return getGroupObjectPath(objectPath, groupId.serialize());
1117 }
1118
1119 @Override
1120 public void quitGroup() throws Error.Failure {
1121 try {
1122 m.quitGroup(groupId, Set.of());
1123 } catch (GroupNotFoundException | NotAGroupMemberException e) {
1124 throw new Error.GroupNotFound(e.getMessage());
1125 } catch (IOException e) {
1126 throw new Error.Failure(e.getMessage());
1127 } catch (LastGroupAdminException e) {
1128 throw new Error.LastGroupAdmin(e.getMessage());
1129 }
1130 }
1131
1132 @Override
1133 public void addMembers(final List<String> recipients) throws Error.Failure {
1134 final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
1135 updateGroup(UpdateGroup.newBuilder().withMembers(memberIdentifiers).build());
1136 }
1137
1138 @Override
1139 public void removeMembers(final List<String> recipients) throws Error.Failure {
1140 final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
1141 updateGroup(UpdateGroup.newBuilder().withRemoveMembers(memberIdentifiers).build());
1142 }
1143
1144 @Override
1145 public void addAdmins(final List<String> recipients) throws Error.Failure {
1146 final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
1147 updateGroup(UpdateGroup.newBuilder().withAdmins(memberIdentifiers).build());
1148 }
1149
1150 @Override
1151 public void removeAdmins(final List<String> recipients) throws Error.Failure {
1152 final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
1153 updateGroup(UpdateGroup.newBuilder().withRemoveAdmins(memberIdentifiers).build());
1154 }
1155
1156 @Override
1157 public void resetLink() throws Error.Failure {
1158 updateGroup(UpdateGroup.newBuilder().withResetGroupLink(true).build());
1159 }
1160
1161 @Override
1162 public void disableLink() throws Error.Failure {
1163 updateGroup(UpdateGroup.newBuilder().withGroupLinkState(GroupLinkState.DISABLED).build());
1164 }
1165
1166 @Override
1167 public void enableLink(final boolean requiresApproval) throws Error.Failure {
1168 updateGroup(UpdateGroup.newBuilder()
1169 .withGroupLinkState(requiresApproval
1170 ? GroupLinkState.ENABLED_WITH_APPROVAL
1171 : GroupLinkState.ENABLED)
1172 .build());
1173 }
1174
1175 private org.asamk.signal.manager.api.Group getGroup() {
1176 return m.getGroup(groupId);
1177 }
1178
1179 private void setGroupName(final String name) {
1180 updateGroup(UpdateGroup.newBuilder().withName(name).build());
1181 }
1182
1183 private void setGroupDescription(final String description) {
1184 updateGroup(UpdateGroup.newBuilder().withDescription(description).build());
1185 }
1186
1187 private void setGroupAvatar(final String avatar) {
1188 updateGroup(UpdateGroup.newBuilder().withAvatarFile(new File(avatar)).build());
1189 }
1190
1191 private void setMessageExpirationTime(final int expirationTime) {
1192 updateGroup(UpdateGroup.newBuilder().withExpirationTimer(expirationTime).build());
1193 }
1194
1195 private void setGroupPermissionAddMember(final String permission) {
1196 updateGroup(UpdateGroup.newBuilder().withAddMemberPermission(GroupPermission.valueOf(permission)).build());
1197 }
1198
1199 private void setGroupPermissionEditDetails(final String permission) {
1200 updateGroup(UpdateGroup.newBuilder()
1201 .withEditDetailsPermission(GroupPermission.valueOf(permission))
1202 .build());
1203 }
1204
1205 private void setGroupPermissionSendMessage(final String permission) {
1206 updateGroup(UpdateGroup.newBuilder()
1207 .withIsAnnouncementGroup(GroupPermission.valueOf(permission) == GroupPermission.ONLY_ADMINS)
1208 .build());
1209 }
1210
1211 private void setIsBlocked(final boolean isBlocked) {
1212 try {
1213 m.setGroupBlocked(groupId, isBlocked);
1214 } catch (NotMasterDeviceException e) {
1215 throw new Error.Failure("This command doesn't work on linked devices.");
1216 } catch (GroupNotFoundException e) {
1217 throw new Error.GroupNotFound(e.getMessage());
1218 } catch (IOException e) {
1219 throw new Error.Failure(e.getMessage());
1220 }
1221 }
1222
1223 private void updateGroup(final UpdateGroup updateGroup) {
1224 try {
1225 m.updateGroup(groupId, updateGroup);
1226 } catch (IOException e) {
1227 throw new Error.Failure(e.getMessage());
1228 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
1229 throw new Error.GroupNotFound(e.getMessage());
1230 } catch (AttachmentInvalidException e) {
1231 throw new Error.AttachmentInvalid(e.getMessage());
1232 }
1233 }
1234 }
1235 }