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