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