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