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