1 package org
.asamk
.signal
.dbus
;
3 import org
.asamk
.Signal
;
4 import org
.asamk
.signal
.BaseConfig
;
5 import org
.asamk
.signal
.commands
.exceptions
.CommandException
;
6 import org
.asamk
.signal
.commands
.exceptions
.IOErrorException
;
7 import org
.asamk
.signal
.manager
.AttachmentInvalidException
;
8 import org
.asamk
.signal
.manager
.Manager
;
9 import org
.asamk
.signal
.manager
.NotMasterDeviceException
;
10 import org
.asamk
.signal
.manager
.StickerPackInvalidException
;
11 import org
.asamk
.signal
.manager
.UntrustedIdentityException
;
12 import org
.asamk
.signal
.manager
.api
.Configuration
;
13 import org
.asamk
.signal
.manager
.api
.Identity
;
14 import org
.asamk
.signal
.manager
.api
.Message
;
15 import org
.asamk
.signal
.manager
.api
.RecipientIdentifier
;
16 import org
.asamk
.signal
.manager
.api
.TypingAction
;
17 import org
.asamk
.signal
.manager
.api
.UpdateGroup
;
18 import org
.asamk
.signal
.manager
.groups
.GroupId
;
19 import org
.asamk
.signal
.manager
.groups
.GroupInviteLinkUrl
;
20 import org
.asamk
.signal
.manager
.groups
.GroupLinkState
;
21 import org
.asamk
.signal
.manager
.groups
.GroupNotFoundException
;
22 import org
.asamk
.signal
.manager
.groups
.GroupPermission
;
23 import org
.asamk
.signal
.manager
.groups
.GroupSendingNotAllowedException
;
24 import org
.asamk
.signal
.manager
.groups
.LastGroupAdminException
;
25 import org
.asamk
.signal
.manager
.groups
.NotAGroupMemberException
;
26 import org
.asamk
.signal
.manager
.storage
.recipients
.Profile
;
27 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientAddress
;
28 import org
.asamk
.signal
.util
.ErrorUtils
;
29 import org
.freedesktop
.dbus
.DBusPath
;
30 import org
.freedesktop
.dbus
.connections
.impl
.DBusConnection
;
31 import org
.freedesktop
.dbus
.exceptions
.DBusException
;
32 import org
.freedesktop
.dbus
.exceptions
.DBusExecutionException
;
33 import org
.freedesktop
.dbus
.types
.Variant
;
34 import org
.slf4j
.Logger
;
35 import org
.slf4j
.LoggerFactory
;
36 import org
.whispersystems
.libsignal
.InvalidKeyException
;
37 import org
.whispersystems
.libsignal
.util
.Pair
;
38 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
39 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
40 import org
.whispersystems
.signalservice
.api
.messages
.SendMessageResult
;
41 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.UnregisteredUserException
;
42 import org
.whispersystems
.signalservice
.api
.util
.InvalidNumberException
;
43 import org
.whispersystems
.signalservice
.internal
.contacts
.crypto
.UnauthenticatedResponseException
;
46 import java
.io
.IOException
;
48 import java
.net
.URISyntaxException
;
49 import java
.util
.ArrayList
;
50 import java
.util
.Arrays
;
51 import java
.util
.Base64
;
52 import java
.util
.Collection
;
53 import java
.util
.HashSet
;
54 import java
.util
.List
;
56 import java
.util
.Objects
;
58 import java
.util
.UUID
;
59 import java
.util
.stream
.Collectors
;
60 import java
.util
.stream
.Stream
;
62 public class DbusSignalImpl
implements Signal
{
64 private final Manager m
;
65 private final DBusConnection connection
;
66 private final String objectPath
;
68 private DBusPath thisDevice
;
69 private final List
<StructDevice
> devices
= new ArrayList
<>();
70 private final List
<StructGroup
> groups
= new ArrayList
<>();
72 private final static Logger logger
= LoggerFactory
.getLogger(DbusSignalImpl
.class);
74 public DbusSignalImpl(final Manager m
, DBusConnection connection
, final String objectPath
) {
76 this.connection
= connection
;
77 this.objectPath
= objectPath
;
80 public void initObjects() {
83 updateConfiguration();
89 unExportConfiguration();
93 public String
getObjectPath() {
98 public String
getSelfNumber() {
99 return m
.getSelfNumber();
103 public void submitRateLimitChallenge(String challenge
, String captchaString
) throws IOErrorException
{
104 final var captcha
= captchaString
== null ?
null : captchaString
.replace("signalcaptcha://", "");
107 m
.submitRateLimitRecaptchaChallenge(challenge
, captcha
);
108 } catch (IOException e
) {
109 throw new IOErrorException("Submit challenge error: " + e
.getMessage(), e
);
115 public void addDevice(String uri
) {
117 m
.addDeviceLink(new URI(uri
));
118 } catch (IOException
| InvalidKeyException e
) {
119 throw new Error
.Failure(e
.getClass().getSimpleName() + " Add device link failed. " + e
.getMessage());
120 } catch (URISyntaxException e
) {
121 throw new Error
.InvalidUri(e
.getClass().getSimpleName()
122 + " Device link uri has invalid format: "
128 public DBusPath
getDevice(long deviceId
) {
130 final var deviceOptional
= devices
.stream().filter(g
-> g
.getId().equals(deviceId
)).findFirst();
131 if (deviceOptional
.isEmpty()) {
132 throw new Error
.DeviceNotFound("Device not found");
134 return deviceOptional
.get().getObjectPath();
138 public List
<StructDevice
> listDevices() {
144 public DBusPath
getThisDevice() {
150 public long sendMessage(final String message
, final List
<String
> attachments
, final String recipient
) {
151 var recipients
= new ArrayList
<String
>(1);
152 recipients
.add(recipient
);
153 return sendMessage(message
, attachments
, recipients
);
157 public long sendMessage(final String message
, final List
<String
> attachments
, final List
<String
> recipients
) {
159 final var results
= m
.sendMessage(new Message(message
, attachments
),
160 getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber()).stream()
161 .map(RecipientIdentifier
.class::cast
)
162 .collect(Collectors
.toSet()));
164 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
165 return results
.getTimestamp();
166 } catch (AttachmentInvalidException e
) {
167 throw new Error
.AttachmentInvalid(e
.getMessage());
168 } catch (IOException e
) {
169 throw new Error
.Failure(e
.getMessage());
170 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
171 throw new Error
.GroupNotFound(e
.getMessage());
176 public long sendRemoteDeleteMessage(
177 final long targetSentTimestamp
, final String recipient
179 var recipients
= new ArrayList
<String
>(1);
180 recipients
.add(recipient
);
181 return sendRemoteDeleteMessage(targetSentTimestamp
, recipients
);
185 public long sendRemoteDeleteMessage(
186 final long targetSentTimestamp
, final List
<String
> recipients
189 final var results
= m
.sendRemoteDeleteMessage(targetSentTimestamp
,
190 getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber()).stream()
191 .map(RecipientIdentifier
.class::cast
)
192 .collect(Collectors
.toSet()));
193 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
194 return results
.getTimestamp();
195 } catch (IOException e
) {
196 throw new Error
.Failure(e
.getMessage());
197 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
198 throw new Error
.GroupNotFound(e
.getMessage());
203 public long sendGroupRemoteDeleteMessage(
204 final long targetSentTimestamp
, final byte[] groupId
207 final var results
= m
.sendRemoteDeleteMessage(targetSentTimestamp
,
208 Set
.of(new RecipientIdentifier
.Group(getGroupId(groupId
))));
209 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
210 return results
.getTimestamp();
211 } catch (IOException e
) {
212 throw new Error
.Failure(e
.getMessage());
213 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
214 throw new Error
.GroupNotFound(e
.getMessage());
219 public long sendMessageReaction(
221 final boolean remove
,
222 final String targetAuthor
,
223 final long targetSentTimestamp
,
224 final String recipient
226 var recipients
= new ArrayList
<String
>(1);
227 recipients
.add(recipient
);
228 return sendMessageReaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
, recipients
);
232 public long sendMessageReaction(
234 final boolean remove
,
235 final String targetAuthor
,
236 final long targetSentTimestamp
,
237 final List
<String
> recipients
240 final var results
= m
.sendMessageReaction(emoji
,
242 getSingleRecipientIdentifier(targetAuthor
, m
.getSelfNumber()),
244 getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber()).stream()
245 .map(RecipientIdentifier
.class::cast
)
246 .collect(Collectors
.toSet()));
247 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
248 return results
.getTimestamp();
249 } catch (IOException e
) {
250 throw new Error
.Failure(e
.getMessage());
251 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
252 throw new Error
.GroupNotFound(e
.getMessage());
257 public void sendTyping(
258 final String recipient
, final boolean stop
259 ) throws Error
.Failure
, Error
.GroupNotFound
, Error
.UntrustedIdentity
{
261 var recipients
= new ArrayList
<String
>(1);
262 recipients
.add(recipient
);
263 m
.sendTypingMessage(stop ? TypingAction
.STOP
: TypingAction
.START
,
264 getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber()).stream()
265 .map(RecipientIdentifier
.class::cast
)
266 .collect(Collectors
.toSet()));
267 } catch (IOException e
) {
268 throw new Error
.Failure(e
.getMessage());
269 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
270 throw new Error
.GroupNotFound(e
.getMessage());
271 } catch (UntrustedIdentityException e
) {
272 throw new Error
.UntrustedIdentity(e
.getMessage());
277 public void sendReadReceipt(
278 final String recipient
, final List
<Long
> messageIds
279 ) throws Error
.Failure
, Error
.UntrustedIdentity
{
281 m
.sendReadReceipt(getSingleRecipientIdentifier(recipient
, m
.getSelfNumber()), messageIds
);
282 } catch (IOException e
) {
283 throw new Error
.Failure(e
.getMessage());
284 } catch (UntrustedIdentityException e
) {
285 throw new Error
.UntrustedIdentity(e
.getMessage());
290 public void sendContacts() {
293 } catch (IOException e
) {
294 throw new Error
.Failure("SendContacts error: " + e
.getMessage());
299 public void sendSyncRequest() {
301 m
.requestAllSyncData();
302 } catch (IOException e
) {
303 throw new Error
.Failure("Request sync data error: " + e
.getMessage());
308 public long sendNoteToSelfMessage(
309 final String message
, final List
<String
> attachments
310 ) throws Error
.AttachmentInvalid
, Error
.Failure
, Error
.UntrustedIdentity
{
312 final var results
= m
.sendMessage(new Message(message
, attachments
),
313 Set
.of(RecipientIdentifier
.NoteToSelf
.INSTANCE
));
314 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
315 return results
.getTimestamp();
316 } catch (AttachmentInvalidException e
) {
317 throw new Error
.AttachmentInvalid(e
.getMessage());
318 } catch (IOException e
) {
319 throw new Error
.Failure(e
.getMessage());
320 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
321 throw new Error
.GroupNotFound(e
.getMessage());
326 public void sendEndSessionMessage(final List
<String
> recipients
) {
328 final var results
= m
.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber()));
329 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
330 } catch (IOException e
) {
331 throw new Error
.Failure(e
.getMessage());
336 public long sendGroupMessage(final String message
, final List
<String
> attachments
, final byte[] groupId
) {
338 var results
= m
.sendMessage(new Message(message
, attachments
),
339 Set
.of(new RecipientIdentifier
.Group(getGroupId(groupId
))));
340 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
341 return results
.getTimestamp();
342 } catch (IOException e
) {
343 throw new Error
.Failure(e
.getMessage());
344 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
345 throw new Error
.GroupNotFound(e
.getMessage());
346 } catch (AttachmentInvalidException e
) {
347 throw new Error
.AttachmentInvalid(e
.getMessage());
352 public long sendGroupMessageReaction(
354 final boolean remove
,
355 final String targetAuthor
,
356 final long targetSentTimestamp
,
360 final var results
= m
.sendMessageReaction(emoji
,
362 getSingleRecipientIdentifier(targetAuthor
, m
.getSelfNumber()),
364 Set
.of(new RecipientIdentifier
.Group(getGroupId(groupId
))));
365 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
366 return results
.getTimestamp();
367 } catch (IOException e
) {
368 throw new Error
.Failure(e
.getMessage());
369 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
370 throw new Error
.GroupNotFound(e
.getMessage());
374 // Since contact names might be empty if not defined, also potentially return
377 public String
getContactName(final String number
) {
378 final var name
= m
.getContactOrProfileName(getSingleRecipientIdentifier(number
, m
.getSelfNumber()));
379 return name
== null ?
"" : name
;
383 public void setContactName(final String number
, final String name
) {
385 m
.setContactName(getSingleRecipientIdentifier(number
, m
.getSelfNumber()), name
);
386 } catch (NotMasterDeviceException e
) {
387 throw new Error
.Failure("This command doesn't work on linked devices.");
388 } catch (UnregisteredUserException e
) {
389 throw new Error
.Failure("Contact is not registered.");
394 public void setExpirationTimer(final String number
, final int expiration
) {
396 m
.setExpirationTimer(getSingleRecipientIdentifier(number
, m
.getSelfNumber()), expiration
);
397 } catch (IOException e
) {
398 throw new Error
.Failure(e
.getMessage());
403 public void setContactBlocked(final String number
, final boolean blocked
) {
405 m
.setContactBlocked(getSingleRecipientIdentifier(number
, m
.getSelfNumber()), blocked
);
406 } catch (NotMasterDeviceException e
) {
407 throw new Error
.Failure("This command doesn't work on linked devices.");
408 } catch (IOException e
) {
409 throw new Error
.Failure(e
.getMessage());
414 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) {
416 m
.setGroupBlocked(getGroupId(groupId
), blocked
);
417 } catch (NotMasterDeviceException e
) {
418 throw new Error
.Failure("This command doesn't work on linked devices.");
419 } catch (GroupNotFoundException e
) {
420 throw new Error
.GroupNotFound(e
.getMessage());
421 } catch (IOException e
) {
422 throw new Error
.Failure(e
.getMessage());
427 public List
<byte[]> getGroupIds() {
428 var groups
= m
.getGroups();
429 var ids
= new ArrayList
<byte[]>(groups
.size());
430 for (var group
: groups
) {
431 ids
.add(group
.getGroupId().serialize());
437 public DBusPath
getGroup(final byte[] groupId
) {
439 final var groupOptional
= groups
.stream().filter(g
-> Arrays
.equals(g
.getId(), groupId
)).findFirst();
440 if (groupOptional
.isEmpty()) {
441 throw new Error
.GroupNotFound("Group not found");
443 return groupOptional
.get().getObjectPath();
447 public List
<StructGroup
> listGroups() {
453 public String
getGroupName(final byte[] groupId
) {
454 var group
= m
.getGroup(getGroupId(groupId
));
455 if (group
== null || group
.getTitle() == null) {
458 return group
.getTitle();
463 public List
<String
> getGroupMembers(final byte[] groupId
) {
464 var group
= m
.getGroup(getGroupId(groupId
));
468 final var members
= group
.getMembers();
469 return getRecipientStrings(members
);
474 public byte[] createGroup(
475 final String name
, final List
<String
> members
, final String avatar
476 ) throws Error
.AttachmentInvalid
, Error
.Failure
, Error
.InvalidNumber
{
477 return updateGroup(new byte[0], name
, members
, avatar
);
481 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) {
483 groupId
= nullIfEmpty(groupId
);
484 name
= nullIfEmpty(name
);
485 avatar
= nullIfEmpty(avatar
);
486 final var memberIdentifiers
= getSingleRecipientIdentifiers(members
, m
.getSelfNumber());
487 if (groupId
== null) {
488 final var results
= m
.createGroup(name
, memberIdentifiers
, avatar
== null ?
null : new File(avatar
));
489 checkSendMessageResults(results
.second().getTimestamp(), results
.second().getResults());
490 return results
.first().serialize();
492 final var results
= m
.updateGroup(getGroupId(groupId
),
493 UpdateGroup
.newBuilder()
495 .withMembers(memberIdentifiers
)
496 .withAvatarFile(avatar
== null ?
null : new File(avatar
))
498 if (results
!= null) {
499 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
503 } catch (IOException e
) {
504 throw new Error
.Failure(e
.getMessage());
505 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
506 throw new Error
.GroupNotFound(e
.getMessage());
507 } catch (AttachmentInvalidException e
) {
508 throw new Error
.AttachmentInvalid(e
.getMessage());
513 public boolean isRegistered() {
518 public boolean isRegistered(String number
) {
519 var result
= isRegistered(List
.of(number
));
520 return result
.get(0);
524 public List
<Boolean
> isRegistered(List
<String
> numbers
) {
525 var results
= new ArrayList
<Boolean
>();
526 if (numbers
.isEmpty()) {
530 Map
<String
, Pair
<String
, UUID
>> registered
;
532 registered
= m
.areUsersRegistered(new HashSet
<>(numbers
));
533 } catch (IOException e
) {
534 throw new Error
.Failure(e
.getMessage());
537 return numbers
.stream().map(number
-> {
538 var uuid
= registered
.get(number
).second();
540 }).collect(Collectors
.toList());
544 public void updateProfile(
550 final boolean removeAvatar
553 givenName
= nullIfEmpty(givenName
);
554 familyName
= nullIfEmpty(familyName
);
555 about
= nullIfEmpty(about
);
556 aboutEmoji
= nullIfEmpty(aboutEmoji
);
557 avatarPath
= nullIfEmpty(avatarPath
);
558 Optional
<File
> avatarFile
= removeAvatar
560 : avatarPath
== null ?
null : Optional
.of(new File(avatarPath
));
561 m
.setProfile(givenName
, familyName
, about
, aboutEmoji
, avatarFile
);
562 } catch (IOException e
) {
563 throw new Error
.Failure(e
.getMessage());
568 public void updateProfile(
571 final String aboutEmoji
,
573 final boolean removeAvatar
575 updateProfile(name
, "", about
, aboutEmoji
, avatarPath
, removeAvatar
);
579 public void removePin() {
581 m
.setRegistrationLockPin(Optional
.absent());
582 } catch (UnauthenticatedResponseException e
) {
583 throw new Error
.Failure("Remove pin failed with unauthenticated response: " + e
.getMessage());
584 } catch (IOException e
) {
585 throw new Error
.Failure("Remove pin error: " + e
.getMessage());
590 public void setPin(String registrationLockPin
) {
592 m
.setRegistrationLockPin(Optional
.of(registrationLockPin
));
593 } catch (UnauthenticatedResponseException e
) {
594 throw new Error
.Failure("Set pin error failed with unauthenticated response: " + e
.getMessage());
595 } catch (IOException e
) {
596 throw new Error
.Failure("Set pin error: " + e
.getMessage());
600 // Provide option to query a version string in order to react on potential
601 // future interface changes
603 public String
version() {
604 return BaseConfig
.PROJECT_VERSION
;
607 // Create a unique list of Numbers from Identities and Contacts to really get
608 // all numbers the system knows
610 public List
<String
> listNumbers() {
611 return Stream
.concat(m
.getIdentities().stream().map(Identity
::getRecipient
),
612 m
.getContacts().stream().map(Pair
::first
))
613 .map(a
-> a
.getNumber().orElse(null))
614 .filter(Objects
::nonNull
)
616 .collect(Collectors
.toList());
620 public List
<String
> getContactNumber(final String name
) {
621 // Contact names have precedence.
622 var numbers
= new ArrayList
<String
>();
623 var contacts
= m
.getContacts();
624 for (var c
: contacts
) {
625 if (name
.equals(c
.second().getName())) {
626 numbers
.add(c
.first().getLegacyIdentifier());
629 // Try profiles if no contact name was found
630 for (var identity
: m
.getIdentities()) {
631 final var address
= identity
.getRecipient();
632 var number
= address
.getNumber().orElse(null);
633 if (number
!= null) {
634 Profile profile
= null;
636 profile
= m
.getRecipientProfile(RecipientIdentifier
.Single
.fromAddress(address
));
637 } catch (UnregisteredUserException ignored
) {
639 if (profile
!= null && profile
.getDisplayName().equals(name
)) {
648 public void quitGroup(final byte[] groupId
) {
649 var group
= getGroupId(groupId
);
651 m
.quitGroup(group
, Set
.of());
652 } catch (GroupNotFoundException
| NotAGroupMemberException e
) {
653 throw new Error
.GroupNotFound(e
.getMessage());
654 } catch (IOException
| LastGroupAdminException e
) {
655 throw new Error
.Failure(e
.getMessage());
660 public byte[] joinGroup(final String groupLink
) {
662 final var linkUrl
= GroupInviteLinkUrl
.fromUri(groupLink
);
663 if (linkUrl
== null) {
664 throw new Error
.Failure("Group link is invalid:");
666 final var result
= m
.joinGroup(linkUrl
);
667 return result
.first().serialize();
668 } catch (GroupInviteLinkUrl
.InvalidGroupLinkException
| GroupLinkNotActiveException e
) {
669 throw new Error
.Failure("Group link is invalid: " + e
.getMessage());
670 } catch (GroupInviteLinkUrl
.UnknownGroupLinkVersionException e
) {
671 throw new Error
.Failure("Group link was created with an incompatible version: " + e
.getMessage());
672 } catch (IOException e
) {
673 throw new Error
.Failure(e
.getMessage());
678 public boolean isContactBlocked(final String number
) {
679 return m
.isContactBlocked(getSingleRecipientIdentifier(number
, m
.getSelfNumber()));
683 public boolean isGroupBlocked(final byte[] groupId
) {
684 var group
= m
.getGroup(getGroupId(groupId
));
688 return group
.isBlocked();
693 public boolean isMember(final byte[] groupId
) {
694 var group
= m
.getGroup(getGroupId(groupId
));
698 return group
.isMember();
703 public String
uploadStickerPack(String stickerPackPath
) {
704 File path
= new File(stickerPackPath
);
706 return m
.uploadStickerPack(path
).toString();
707 } catch (IOException e
) {
708 throw new Error
.IOError("Upload error (maybe image size is too large):" + e
.getMessage());
709 } catch (StickerPackInvalidException e
) {
710 throw new Error
.Failure("Invalid sticker pack: " + e
.getMessage());
714 private static void checkSendMessageResult(long timestamp
, SendMessageResult result
) throws DBusExecutionException
{
715 var error
= ErrorUtils
.getErrorMessageFromSendMessageResult(result
);
721 final var message
= timestamp
+ "\nFailed to send message:\n" + error
+ '\n';
723 if (result
.getIdentityFailure() != null) {
724 throw new Error
.UntrustedIdentity(message
);
726 throw new Error
.Failure(message
);
730 private static void checkSendMessageResults(
731 long timestamp
, Map
<RecipientIdentifier
, List
<SendMessageResult
>> results
732 ) throws DBusExecutionException
{
733 final var sendMessageResults
= results
.values().stream().findFirst();
734 if (results
.size() == 1 && sendMessageResults
.get().size() == 1) {
735 checkSendMessageResult(timestamp
, sendMessageResults
.get().stream().findFirst().get());
739 var errors
= ErrorUtils
.getErrorMessagesFromSendMessageResults(results
);
740 if (errors
.size() == 0) {
744 var message
= new StringBuilder();
745 message
.append(timestamp
).append('\n');
746 message
.append("Failed to send (some) messages:\n");
747 for (var error
: errors
) {
748 message
.append(error
).append('\n');
751 throw new Error
.Failure(message
.toString());
754 private static void checkSendMessageResults(
755 long timestamp
, Collection
<SendMessageResult
> results
756 ) throws DBusExecutionException
{
757 if (results
.size() == 1) {
758 checkSendMessageResult(timestamp
, results
.stream().findFirst().get());
762 var errors
= ErrorUtils
.getErrorMessagesFromSendMessageResults(results
);
763 if (errors
.size() == 0) {
767 var message
= new StringBuilder();
768 message
.append(timestamp
).append('\n');
769 message
.append("Failed to send (some) messages:\n");
770 for (var error
: errors
) {
771 message
.append(error
).append('\n');
774 throw new Error
.Failure(message
.toString());
777 private static List
<String
> getRecipientStrings(final Set
<RecipientAddress
> members
) {
778 return members
.stream().map(RecipientAddress
::getLegacyIdentifier
).collect(Collectors
.toList());
781 private static Set
<RecipientIdentifier
.Single
> getSingleRecipientIdentifiers(
782 final Collection
<String
> recipientStrings
, final String localNumber
783 ) throws DBusExecutionException
{
784 final var identifiers
= new HashSet
<RecipientIdentifier
.Single
>();
785 for (var recipientString
: recipientStrings
) {
786 identifiers
.add(getSingleRecipientIdentifier(recipientString
, localNumber
));
791 private static RecipientIdentifier
.Single
getSingleRecipientIdentifier(
792 final String recipientString
, final String localNumber
793 ) throws DBusExecutionException
{
795 return RecipientIdentifier
.Single
.fromString(recipientString
, localNumber
);
796 } catch (InvalidNumberException e
) {
797 throw new Error
.InvalidNumber(e
.getMessage());
801 private static GroupId
getGroupId(byte[] groupId
) throws DBusExecutionException
{
803 return GroupId
.unknownVersion(groupId
);
804 } catch (Throwable e
) {
805 throw new Error
.InvalidGroupId("Invalid group id: " + e
.getMessage());
809 private byte[] nullIfEmpty(final byte[] array
) {
810 return array
.length
== 0 ?
null : array
;
813 private String
nullIfEmpty(final String name
) {
814 return name
.isEmpty() ?
null : name
;
817 private String
emptyIfNull(final String string
) {
818 return string
== null ?
"" : string
;
821 private static String
getDeviceObjectPath(String basePath
, long deviceId
) {
822 return basePath
+ "/Devices/" + deviceId
;
825 private void updateDevices() {
826 List
<org
.asamk
.signal
.manager
.api
.Device
> linkedDevices
;
828 linkedDevices
= m
.getLinkedDevices();
829 } catch (IOException e
) {
830 throw new Error
.Failure("Failed to get linked devices: " + e
.getMessage());
835 linkedDevices
.forEach(d
-> {
836 final var object
= new DbusSignalDeviceImpl(d
);
837 final var deviceObjectPath
= object
.getObjectPath();
839 connection
.exportObject(object
);
840 logger
.info("Exported dbus object: " + deviceObjectPath
);
841 } catch (DBusException e
) {
844 if (d
.isThisDevice()) {
845 thisDevice
= new DBusPath(deviceObjectPath
);
847 this.devices
.add(new StructDevice(new DBusPath(deviceObjectPath
), d
.getId(), emptyIfNull(d
.getName())));
851 private void unExportDevices() {
852 this.devices
.stream()
853 .map(StructDevice
::getObjectPath
)
854 .map(DBusPath
::getPath
)
855 .forEach(connection
::unExportObject
);
856 this.devices
.clear();
859 private static String
getGroupObjectPath(String basePath
, byte[] groupId
) {
860 return basePath
+ "/Groups/" + Base64
.getEncoder()
861 .encodeToString(groupId
)
867 private void updateGroups() {
868 List
<org
.asamk
.signal
.manager
.api
.Group
> groups
;
869 groups
= m
.getGroups();
873 groups
.forEach(g
-> {
874 final var object
= new DbusSignalGroupImpl(g
.getGroupId());
876 connection
.exportObject(object
);
877 logger
.info("Exported dbus object: " + object
.getObjectPath());
878 } catch (DBusException e
) {
881 this.groups
.add(new StructGroup(new DBusPath(object
.getObjectPath()),
882 g
.getGroupId().serialize(),
883 emptyIfNull(g
.getTitle())));
887 private void unExportGroups() {
888 this.groups
.stream().map(StructGroup
::getObjectPath
).map(DBusPath
::getPath
).forEach(connection
::unExportObject
);
892 private void unExportConfiguration() {
893 final var object
= getConfigurationObjectPath(objectPath
);
894 connection
.unExportObject(object
);
897 private static String
getConfigurationObjectPath(String basePath
) {
898 return basePath
+ "/Configuration";
901 private void updateConfiguration() {
903 Boolean readReceipts
= null;
904 Boolean unidentifiedDeliveryIndicators
= null;
905 Boolean typingIndicators
= null;
906 Boolean linkPreviews
= null;
907 List
<Boolean
> configuration
= new ArrayList
<>(4);
910 configuration
= m
.getConfiguration();
911 } catch (NotMasterDeviceException e
) {
912 logger
.info("Not exporting Configuration for " + m
.getSelfNumber() + ": " + e
.getMessage());
914 } catch (IOException e
) {
915 throw new Error
.IOError(objectPath
+ e
.getMessage());
916 } catch (NullPointerException e
) {
917 logger
.info("No configuration found, creating one for " + m
.getSelfNumber() + ": " + e
.getMessage());
919 unidentifiedDeliveryIndicators
= true;
920 typingIndicators
= true;
923 if (readReceipts
== null) {
925 readReceipts
= configuration
.get(0);
926 } catch (NullPointerException e
) {
930 if (unidentifiedDeliveryIndicators
== null) {
932 unidentifiedDeliveryIndicators
= configuration
.get(1);
933 } catch (NullPointerException e
) {
934 unidentifiedDeliveryIndicators
= true;
937 if (typingIndicators
== null) {
939 typingIndicators
= configuration
.get(2);
940 } catch (NullPointerException e
) {
941 typingIndicators
= true;
944 if (linkPreviews
== null) {
946 linkPreviews
= configuration
.get(3);
947 } catch (NullPointerException e
) {
953 unExportConfiguration();
954 m
.updateConfiguration(readReceipts
, unidentifiedDeliveryIndicators
, typingIndicators
, linkPreviews
);
955 final var object
= new DbusSignalConfigurationImpl(readReceipts
, unidentifiedDeliveryIndicators
, typingIndicators
, linkPreviews
);
956 connection
.exportObject(object
);
957 logger
.info("Exported dbus object: " + objectPath
+ "/Configuration");
958 } catch (NotMasterDeviceException ignore
) {
960 } catch (IOException e
) {
961 throw new Error
.IOError(objectPath
+ e
.getMessage());
962 } catch (DBusException e
) {
968 public class DbusSignalDeviceImpl
extends DbusProperties
implements Signal
.Device
{
970 private final org
.asamk
.signal
.manager
.api
.Device device
;
972 public DbusSignalDeviceImpl(final org
.asamk
.signal
.manager
.api
.Device device
) {
973 super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Device",
974 List
.of(new DbusProperty
<>("Id", device
::getId
),
975 new DbusProperty
<>("Name", () -> emptyIfNull(device
.getName()), this::setDeviceName
),
976 new DbusProperty
<>("Created", device
::getCreated
),
977 new DbusProperty
<>("LastSeen", device
::getLastSeen
))));
978 this.device
= device
;
982 public String
getObjectPath() {
983 return getDeviceObjectPath(objectPath
, device
.getId());
987 public void removeDevice() throws Error
.Failure
{
989 m
.removeLinkedDevices(device
.getId());
991 } catch (IOException e
) {
992 throw new Error
.Failure(e
.getMessage());
996 private void setDeviceName(String name
) {
997 if (!device
.isThisDevice()) {
998 throw new Error
.Failure("Only the name of this device can be changed");
1001 m
.updateAccountAttributes(name
);
1002 // update device list
1004 } catch (IOException e
) {
1005 throw new Error
.Failure(e
.getMessage());
1010 public class DbusSignalConfigurationImpl
extends DbusProperties
implements Signal
.Configuration
{
1012 private final Boolean readReceipts
;
1013 private final Boolean unidentifiedDeliveryIndicators
;
1014 private final Boolean typingIndicators
;
1015 private final Boolean linkPreviews
;
1017 public DbusSignalConfigurationImpl(final Boolean readReceipts
, final Boolean unidentifiedDeliveryIndicators
, final Boolean typingIndicators
, final Boolean linkPreviews
) {
1018 this.readReceipts
= readReceipts
;
1019 this.unidentifiedDeliveryIndicators
= unidentifiedDeliveryIndicators
;
1020 this.typingIndicators
= typingIndicators
;
1021 this.linkPreviews
= linkPreviews
;
1022 super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Configuration",
1023 List
.of(new DbusProperty
<>("ConfigurationReadReceipts", () -> getReadReceipts(), this::setReadReceipts
),
1024 new DbusProperty
<>("ConfigurationUnidentifiedDeliveryIndicators", () -> getUnidentifiedDeliveryIndicators(), this::setUnidentifiedDeliveryIndicators
),
1025 new DbusProperty
<>("ConfigurationTypingIndicators", () -> getTypingIndicators(), this::setTypingIndicators
),
1026 new DbusProperty
<>("ConfigurationLinkPreviews", () -> getLinkPreviews(), this::setLinkPreviews
)
1033 public String
getObjectPath() {
1034 return getConfigurationObjectPath(objectPath
);
1037 public void setReadReceipts(Boolean readReceipts
) {
1038 setConfiguration(readReceipts
, null, null, null);
1041 public void setUnidentifiedDeliveryIndicators(Boolean unidentifiedDeliveryIndicators
) {
1042 setConfiguration(null, unidentifiedDeliveryIndicators
, null, null);
1045 public void setTypingIndicators(Boolean typingIndicators
) {
1046 setConfiguration(null, null, typingIndicators
, null);
1049 public void setLinkPreviews(Boolean linkPreviews
) {
1050 setConfiguration(null, null, null, linkPreviews
);
1053 private void setConfiguration(Boolean readReceipts
, Boolean unidentifiedDeliveryIndicators
, Boolean typingIndicators
, Boolean linkPreviews
) {
1055 if (readReceipts
== null) {
1056 readReceipts
= m
.getConfiguration().get(0);
1058 if (unidentifiedDeliveryIndicators
== null) {
1059 unidentifiedDeliveryIndicators
= m
.getConfiguration().get(1);
1061 if (typingIndicators
== null) {
1062 typingIndicators
= m
.getConfiguration().get(2);
1064 if (linkPreviews
== null) {
1065 linkPreviews
= m
.getConfiguration().get(3);
1067 m
.updateConfiguration(readReceipts
, unidentifiedDeliveryIndicators
, typingIndicators
, linkPreviews
);
1068 } catch (IOException e
) {
1069 throw new Error
.IOError("UpdateAccount error: " + e
.getMessage());
1070 } catch (NotMasterDeviceException e
) {
1071 throw new Error
.UserError("This command doesn't work on linked devices.");
1075 public List
<Boolean
> getConfiguration() {
1076 List
<Boolean
> config
= new ArrayList
<>(4);
1078 config
= m
.getConfiguration();
1079 } catch (IOException e
) {
1080 throw new Error
.IOError("Configuration storage error: " + e
.getMessage());
1081 } catch (NotMasterDeviceException e
) {
1082 throw new Error
.UserError("This command doesn't work on linked devices.");
1087 public Boolean
getReadReceipts() {
1089 return m
.getConfiguration().get(0);
1090 } catch (IOException e
) {
1091 throw new Error
.IOError("Configuration storage error: " + e
.getMessage());
1092 } catch (NotMasterDeviceException e
) {
1093 throw new Error
.UserError("This command doesn't work on linked devices.");
1097 public Boolean
getUnidentifiedDeliveryIndicators() {
1099 return m
.getConfiguration().get(1);
1100 } catch (IOException e
) {
1101 throw new Error
.IOError("Configuration storage error: " + e
.getMessage());
1102 } catch (NotMasterDeviceException e
) {
1103 throw new Error
.UserError("This command doesn't work on linked devices.");
1107 public Boolean
getTypingIndicators() {
1109 return m
.getConfiguration().get(2);
1110 } catch (IOException e
) {
1111 throw new Error
.IOError("Configuration storage error: " + e
.getMessage());
1112 } catch (NotMasterDeviceException e
) {
1113 throw new Error
.UserError("This command doesn't work on linked devices.");
1117 public Boolean
getLinkPreviews() {
1119 return m
.getConfiguration().get(3);
1120 } catch (IOException e
) {
1121 throw new Error
.IOError("Configuration storage error: " + e
.getMessage());
1122 } catch (NotMasterDeviceException e
) {
1123 throw new Error
.UserError("This command doesn't work on linked devices.");
1128 public class DbusSignalGroupImpl
extends DbusProperties
implements Signal
.Group
{
1130 private final GroupId groupId
;
1132 public DbusSignalGroupImpl(final GroupId groupId
) {
1133 this.groupId
= groupId
;
1134 super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Group",
1135 List
.of(new DbusProperty
<>("Id", groupId
::serialize
),
1136 new DbusProperty
<>("Name", () -> emptyIfNull(getGroup().getTitle()), this::setGroupName
),
1137 new DbusProperty
<>("Description",
1138 () -> emptyIfNull(getGroup().getDescription()),
1139 this::setGroupDescription
),
1140 new DbusProperty
<>("Avatar", this::setGroupAvatar
),
1141 new DbusProperty
<>("IsBlocked", () -> getGroup().isBlocked(), this::setIsBlocked
),
1142 new DbusProperty
<>("IsMember", () -> getGroup().isMember()),
1143 new DbusProperty
<>("IsAdmin", () -> getGroup().isAdmin()),
1144 new DbusProperty
<>("MessageExpirationTimer",
1145 () -> getGroup().getMessageExpirationTimer(),
1146 this::setMessageExpirationTime
),
1147 new DbusProperty
<>("Members",
1148 () -> new Variant
<>(getRecipientStrings(getGroup().getMembers()), "as")),
1149 new DbusProperty
<>("PendingMembers",
1150 () -> new Variant
<>(getRecipientStrings(getGroup().getPendingMembers()), "as")),
1151 new DbusProperty
<>("RequestingMembers",
1152 () -> new Variant
<>(getRecipientStrings(getGroup().getRequestingMembers()), "as")),
1153 new DbusProperty
<>("Admins",
1154 () -> new Variant
<>(getRecipientStrings(getGroup().getAdminMembers()), "as")),
1155 new DbusProperty
<>("PermissionAddMember",
1156 () -> getGroup().getPermissionAddMember().name(),
1157 this::setGroupPermissionAddMember
),
1158 new DbusProperty
<>("PermissionEditDetails",
1159 () -> getGroup().getPermissionEditDetails().name(),
1160 this::setGroupPermissionEditDetails
),
1161 new DbusProperty
<>("PermissionSendMessage",
1162 () -> getGroup().getPermissionSendMessage().name(),
1163 this::setGroupPermissionSendMessage
),
1164 new DbusProperty
<>("GroupInviteLink", () -> {
1165 final var groupInviteLinkUrl
= getGroup().getGroupInviteLinkUrl();
1166 return groupInviteLinkUrl
== null ?
"" : groupInviteLinkUrl
.getUrl();
1171 public String
getObjectPath() {
1172 return getGroupObjectPath(objectPath
, groupId
.serialize());
1176 public void quitGroup() throws Error
.Failure
{
1178 m
.quitGroup(groupId
, Set
.of());
1179 } catch (GroupNotFoundException
| NotAGroupMemberException e
) {
1180 throw new Error
.GroupNotFound(e
.getMessage());
1181 } catch (IOException e
) {
1182 throw new Error
.Failure(e
.getMessage());
1183 } catch (LastGroupAdminException e
) {
1184 throw new Error
.LastGroupAdmin(e
.getMessage());
1189 public void addMembers(final List
<String
> recipients
) throws Error
.Failure
{
1190 final var memberIdentifiers
= getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber());
1191 updateGroup(UpdateGroup
.newBuilder().withMembers(memberIdentifiers
).build());
1195 public void removeMembers(final List
<String
> recipients
) throws Error
.Failure
{
1196 final var memberIdentifiers
= getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber());
1197 updateGroup(UpdateGroup
.newBuilder().withRemoveMembers(memberIdentifiers
).build());
1201 public void addAdmins(final List
<String
> recipients
) throws Error
.Failure
{
1202 final var memberIdentifiers
= getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber());
1203 updateGroup(UpdateGroup
.newBuilder().withAdmins(memberIdentifiers
).build());
1207 public void removeAdmins(final List
<String
> recipients
) throws Error
.Failure
{
1208 final var memberIdentifiers
= getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber());
1209 updateGroup(UpdateGroup
.newBuilder().withRemoveAdmins(memberIdentifiers
).build());
1213 public void resetLink() throws Error
.Failure
{
1214 updateGroup(UpdateGroup
.newBuilder().withResetGroupLink(true).build());
1218 public void disableLink() throws Error
.Failure
{
1219 updateGroup(UpdateGroup
.newBuilder().withGroupLinkState(GroupLinkState
.DISABLED
).build());
1223 public void enableLink(final boolean requiresApproval
) throws Error
.Failure
{
1224 updateGroup(UpdateGroup
.newBuilder()
1225 .withGroupLinkState(requiresApproval
1226 ? GroupLinkState
.ENABLED_WITH_APPROVAL
1227 : GroupLinkState
.ENABLED
)
1231 private org
.asamk
.signal
.manager
.api
.Group
getGroup() {
1232 return m
.getGroup(groupId
);
1235 private void setGroupName(final String name
) {
1236 updateGroup(UpdateGroup
.newBuilder().withName(name
).build());
1239 private void setGroupDescription(final String description
) {
1240 updateGroup(UpdateGroup
.newBuilder().withDescription(description
).build());
1243 private void setGroupAvatar(final String avatar
) {
1244 updateGroup(UpdateGroup
.newBuilder().withAvatarFile(new File(avatar
)).build());
1247 private void setMessageExpirationTime(final int expirationTime
) {
1248 updateGroup(UpdateGroup
.newBuilder().withExpirationTimer(expirationTime
).build());
1251 private void setGroupPermissionAddMember(final String permission
) {
1252 updateGroup(UpdateGroup
.newBuilder().withAddMemberPermission(GroupPermission
.valueOf(permission
)).build());
1255 private void setGroupPermissionEditDetails(final String permission
) {
1256 updateGroup(UpdateGroup
.newBuilder()
1257 .withEditDetailsPermission(GroupPermission
.valueOf(permission
))
1261 private void setGroupPermissionSendMessage(final String permission
) {
1262 updateGroup(UpdateGroup
.newBuilder()
1263 .withIsAnnouncementGroup(GroupPermission
.valueOf(permission
) == GroupPermission
.ONLY_ADMINS
)
1267 private void setIsBlocked(final boolean isBlocked
) {
1269 m
.setGroupBlocked(groupId
, isBlocked
);
1270 } catch (NotMasterDeviceException e
) {
1271 throw new Error
.Failure("This command doesn't work on linked devices.");
1272 } catch (GroupNotFoundException e
) {
1273 throw new Error
.GroupNotFound(e
.getMessage());
1274 } catch (IOException e
) {
1275 throw new Error
.Failure(e
.getMessage());
1279 private void updateGroup(final UpdateGroup updateGroup
) {
1281 m
.updateGroup(groupId
, updateGroup
);
1282 } catch (IOException e
) {
1283 throw new Error
.Failure(e
.getMessage());
1284 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
1285 throw new Error
.GroupNotFound(e
.getMessage());
1286 } catch (AttachmentInvalidException e
) {
1287 throw new Error
.AttachmentInvalid(e
.getMessage());