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