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