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