1 package org
.asamk
.signal
.dbus
;
3 import org
.asamk
.Signal
;
4 import org
.asamk
.signal
.BaseConfig
;
5 import org
.asamk
.signal
.manager
.AttachmentInvalidException
;
6 import org
.asamk
.signal
.manager
.Manager
;
7 import org
.asamk
.signal
.manager
.NotMasterDeviceException
;
8 import org
.asamk
.signal
.manager
.StickerPackInvalidException
;
9 import org
.asamk
.signal
.manager
.UntrustedIdentityException
;
10 import org
.asamk
.signal
.manager
.api
.Device
;
11 import org
.asamk
.signal
.manager
.api
.Message
;
12 import org
.asamk
.signal
.manager
.api
.RecipientIdentifier
;
13 import org
.asamk
.signal
.manager
.api
.TypingAction
;
14 import org
.asamk
.signal
.manager
.groups
.GroupId
;
15 import org
.asamk
.signal
.manager
.groups
.GroupInviteLinkUrl
;
16 import org
.asamk
.signal
.manager
.groups
.GroupNotFoundException
;
17 import org
.asamk
.signal
.manager
.groups
.GroupSendingNotAllowedException
;
18 import org
.asamk
.signal
.manager
.groups
.LastGroupAdminException
;
19 import org
.asamk
.signal
.manager
.groups
.NotAGroupMemberException
;
20 import org
.asamk
.signal
.manager
.storage
.identities
.IdentityInfo
;
21 import org
.asamk
.signal
.util
.ErrorUtils
;
22 import org
.asamk
.signal
.util
.Util
;
23 import org
.freedesktop
.dbus
.exceptions
.DBusExecutionException
;
24 import org
.whispersystems
.libsignal
.InvalidKeyException
;
25 import org
.whispersystems
.libsignal
.util
.Pair
;
26 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
27 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
28 import org
.whispersystems
.signalservice
.api
.messages
.SendMessageResult
;
29 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.UnregisteredUserException
;
30 import org
.whispersystems
.signalservice
.api
.util
.InvalidNumberException
;
31 import org
.whispersystems
.signalservice
.internal
.contacts
.crypto
.UnauthenticatedResponseException
;
34 import java
.io
.IOException
;
36 import java
.net
.URISyntaxException
;
37 import java
.util
.ArrayList
;
38 import java
.util
.Collection
;
39 import java
.util
.HashSet
;
40 import java
.util
.List
;
42 import java
.util
.Objects
;
44 import java
.util
.stream
.Collectors
;
45 import java
.util
.stream
.Stream
;
47 import static org
.asamk
.signal
.util
.Util
.getLegacyIdentifier
;
49 public class DbusSignalImpl
implements Signal
{
51 private final Manager m
;
52 private final String objectPath
;
54 public DbusSignalImpl(final Manager m
, final String objectPath
) {
56 this.objectPath
= objectPath
;
60 public boolean isRemote() {
65 public String
getObjectPath() {
70 public void addDevice(String uri
) {
72 m
.addDeviceLink(new URI(uri
));
73 } catch (IOException
| InvalidKeyException e
) {
74 throw new Error
.Failure(e
.getClass().getSimpleName() + " Add device link failed. " + e
.getMessage());
75 } catch (URISyntaxException e
) {
76 throw new Error
.InvalidUri(e
.getClass().getSimpleName() + " Device link uri has invalid format: " + e
.getMessage());
81 public void removeDevice(int deviceId
) {
83 m
.removeLinkedDevices(deviceId
);
84 } catch (IOException e
) {
85 throw new Error
.Failure(e
.getClass().getSimpleName() + ": Error while removing device: " + e
.getMessage());
90 public List
<String
> listDevices() {
92 List
<String
> results
= new ArrayList
<String
>();
95 devices
= m
.getLinkedDevices();
96 } catch (IOException
| Error
.Failure e
) {
97 throw new Error
.Failure("Failed to get linked devices: " + e
.getMessage());
100 return devices
.stream()
101 .map(d
-> d
.getName() == null ?
"" : d
.getName())
102 .collect(Collectors
.toList());
106 public void updateDeviceName(String deviceName
) {
108 m
.updateAccountAttributes(deviceName
);
109 } catch (IOException
| Signal
.Error
.Failure e
) {
110 throw new Error
.Failure("UpdateAccount error: " + e
.getMessage());
115 public long sendMessage(final String message
, final List
<String
> attachments
, final String recipient
) {
116 var recipients
= new ArrayList
<String
>(1);
117 recipients
.add(recipient
);
118 return sendMessage(message
, attachments
, recipients
);
122 public long sendMessage(final String message
, final List
<String
> attachments
, final List
<String
> recipients
) {
124 final var results
= m
.sendMessage(new Message(message
, attachments
),
125 getSingleRecipientIdentifiers(recipients
, m
.getUsername()).stream()
126 .map(RecipientIdentifier
.class::cast
)
127 .collect(Collectors
.toSet()));
129 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
130 return results
.getTimestamp();
131 } catch (AttachmentInvalidException e
) {
132 throw new Error
.AttachmentInvalid(e
.getMessage());
133 } catch (IOException e
) {
134 throw new Error
.Failure(e
.getMessage());
135 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
136 throw new Error
.GroupNotFound(e
.getMessage());
141 public long sendRemoteDeleteMessage(
142 final long targetSentTimestamp
, final String recipient
144 var recipients
= new ArrayList
<String
>(1);
145 recipients
.add(recipient
);
146 return sendRemoteDeleteMessage(targetSentTimestamp
, recipients
);
150 public long sendRemoteDeleteMessage(
151 final long targetSentTimestamp
, final List
<String
> recipients
154 final var results
= m
.sendRemoteDeleteMessage(targetSentTimestamp
,
155 getSingleRecipientIdentifiers(recipients
, m
.getUsername()).stream()
156 .map(RecipientIdentifier
.class::cast
)
157 .collect(Collectors
.toSet()));
158 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
159 return results
.getTimestamp();
160 } catch (IOException e
) {
161 throw new Error
.Failure(e
.getMessage());
162 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
163 throw new Error
.GroupNotFound(e
.getMessage());
168 public long sendGroupRemoteDeleteMessage(
169 final long targetSentTimestamp
, final byte[] groupId
172 final var results
= m
.sendRemoteDeleteMessage(targetSentTimestamp
,
173 Set
.of(new RecipientIdentifier
.Group(getGroupId(groupId
))));
174 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
175 return results
.getTimestamp();
176 } catch (IOException e
) {
177 throw new Error
.Failure(e
.getMessage());
178 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
179 throw new Error
.GroupNotFound(e
.getMessage());
184 public long sendMessageReaction(
186 final boolean remove
,
187 final String targetAuthor
,
188 final long targetSentTimestamp
,
189 final String recipient
191 var recipients
= new ArrayList
<String
>(1);
192 recipients
.add(recipient
);
193 return sendMessageReaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
, recipients
);
197 public long sendMessageReaction(
199 final boolean remove
,
200 final String targetAuthor
,
201 final long targetSentTimestamp
,
202 final List
<String
> recipients
205 final var results
= m
.sendMessageReaction(emoji
,
207 getSingleRecipientIdentifier(targetAuthor
, m
.getUsername()),
209 getSingleRecipientIdentifiers(recipients
, m
.getUsername()).stream()
210 .map(RecipientIdentifier
.class::cast
)
211 .collect(Collectors
.toSet()));
212 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
213 return results
.getTimestamp();
214 } catch (IOException e
) {
215 throw new Error
.Failure(e
.getMessage());
216 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
217 throw new Error
.GroupNotFound(e
.getMessage());
222 public void sendTyping(
223 final String recipient
, final boolean stop
224 ) throws Error
.Failure
, Error
.GroupNotFound
, Error
.UntrustedIdentity
{
226 var recipients
= new ArrayList
<String
>(1);
227 recipients
.add(recipient
);
228 m
.sendTypingMessage(stop ? TypingAction
.STOP
: TypingAction
.START
,
229 getSingleRecipientIdentifiers(recipients
, m
.getUsername()).stream()
230 .map(RecipientIdentifier
.class::cast
)
231 .collect(Collectors
.toSet()));
232 } catch (IOException e
) {
233 throw new Error
.Failure(e
.getMessage());
234 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
235 throw new Error
.GroupNotFound(e
.getMessage());
236 } catch (UntrustedIdentityException e
) {
237 throw new Error
.UntrustedIdentity(e
.getMessage());
242 public void sendReadReceipt(
243 final String recipient
, final List
<Long
> timestamps
244 ) throws Error
.Failure
, Error
.UntrustedIdentity
{
246 m
.sendReadReceipt(getSingleRecipientIdentifier(recipient
, m
.getUsername()), timestamps
);
247 } catch (IOException e
) {
248 throw new Error
.Failure(e
.getMessage());
249 } catch (UntrustedIdentityException e
) {
250 throw new Error
.UntrustedIdentity(e
.getMessage());
255 public void sendContacts() {
258 } catch (IOException e
) {
259 throw new Error
.Failure("SendContacts error: " + e
.getMessage());
264 public void sendSyncRequest() {
266 m
.requestAllSyncData();
267 } catch (IOException e
) {
268 throw new Error
.Failure("Request sync data error: " + e
.getMessage());
273 public long sendNoteToSelfMessage(
274 final String message
, final List
<String
> attachments
275 ) throws Error
.AttachmentInvalid
, Error
.Failure
, Error
.UntrustedIdentity
{
277 final var results
= m
.sendMessage(new Message(message
, attachments
),
278 Set
.of(new RecipientIdentifier
.NoteToSelf()));
279 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
280 return results
.getTimestamp();
281 } catch (AttachmentInvalidException e
) {
282 throw new Error
.AttachmentInvalid(e
.getMessage());
283 } catch (IOException e
) {
284 throw new Error
.Failure(e
.getMessage());
285 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
286 throw new Error
.GroupNotFound(e
.getMessage());
291 public void sendEndSessionMessage(final List
<String
> recipients
) {
293 final var results
= m
.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients
, m
.getUsername()));
294 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
295 } catch (IOException e
) {
296 throw new Error
.Failure(e
.getMessage());
301 public long sendGroupMessage(final String message
, final List
<String
> attachments
, final byte[] groupId
) {
303 var results
= m
.sendMessage(new Message(message
, attachments
),
304 Set
.of(new RecipientIdentifier
.Group(getGroupId(groupId
))));
305 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
306 return results
.getTimestamp();
307 } catch (IOException e
) {
308 throw new Error
.Failure(e
.getMessage());
309 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
310 throw new Error
.GroupNotFound(e
.getMessage());
311 } catch (AttachmentInvalidException e
) {
312 throw new Error
.AttachmentInvalid(e
.getMessage());
317 public long sendGroupMessageReaction(
319 final boolean remove
,
320 final String targetAuthor
,
321 final long targetSentTimestamp
,
325 final var results
= m
.sendMessageReaction(emoji
,
327 getSingleRecipientIdentifier(targetAuthor
, m
.getUsername()),
329 Set
.of(new RecipientIdentifier
.Group(getGroupId(groupId
))));
330 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
331 return results
.getTimestamp();
332 } catch (IOException e
) {
333 throw new Error
.Failure(e
.getMessage());
334 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
335 throw new Error
.GroupNotFound(e
.getMessage());
339 // Since contact names might be empty if not defined, also potentially return
342 public String
getContactName(final String number
) {
343 return m
.getContactOrProfileName(getSingleRecipientIdentifier(number
, m
.getUsername()));
347 public void setContactName(final String number
, final String name
) {
349 m
.setContactName(getSingleRecipientIdentifier(number
, m
.getUsername()), name
);
350 } catch (NotMasterDeviceException e
) {
351 throw new Error
.Failure("This command doesn't work on linked devices.");
352 } catch (UnregisteredUserException e
) {
353 throw new Error
.Failure("Contact is not registered.");
358 public void setExpirationTimer(final String number
, final int expiration
) {
360 m
.setExpirationTimer(getSingleRecipientIdentifier(number
, m
.getUsername()), expiration
);
361 } catch (IOException e
) {
362 throw new Error
.Failure(e
.getMessage());
367 public void setContactBlocked(final String number
, final boolean blocked
) {
369 m
.setContactBlocked(getSingleRecipientIdentifier(number
, m
.getUsername()), blocked
);
370 } catch (NotMasterDeviceException e
) {
371 throw new Error
.Failure("This command doesn't work on linked devices.");
372 } catch (IOException e
) {
373 throw new Error
.Failure(e
.getMessage());
378 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) {
380 m
.setGroupBlocked(getGroupId(groupId
), blocked
);
381 } catch (GroupNotFoundException e
) {
382 throw new Error
.GroupNotFound(e
.getMessage());
383 } catch (IOException e
) {
384 throw new Error
.Failure(e
.getMessage());
389 public List
<byte[]> getGroupIds() {
390 var groups
= m
.getGroups();
391 var ids
= new ArrayList
<byte[]>(groups
.size());
392 for (var group
: groups
) {
393 ids
.add(group
.getGroupId().serialize());
399 public String
getGroupName(final byte[] groupId
) {
400 var group
= m
.getGroup(getGroupId(groupId
));
404 return group
.getTitle();
409 public List
<String
> getGroupMembers(final byte[] groupId
) {
410 var group
= m
.getGroup(getGroupId(groupId
));
414 return group
.getMembers()
416 .map(m
::resolveSignalServiceAddress
)
417 .map(Util
::getLegacyIdentifier
)
418 .collect(Collectors
.toList());
423 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) {
425 if (groupId
.length
== 0) {
428 if (name
.isEmpty()) {
431 if (avatar
.isEmpty()) {
434 final var memberIdentifiers
= getSingleRecipientIdentifiers(members
, m
.getUsername());
435 if (groupId
== null) {
436 final var results
= m
.createGroup(name
, memberIdentifiers
, avatar
== null ?
null : new File(avatar
));
437 checkSendMessageResults(results
.second().getTimestamp(), results
.second().getResults());
438 return results
.first().serialize();
440 final var results
= m
.updateGroup(getGroupId(groupId
),
451 avatar
== null ?
null : new File(avatar
),
454 if (results
!= null) {
455 checkSendMessageResults(results
.getTimestamp(), results
.getResults());
459 } catch (IOException e
) {
460 throw new Error
.Failure(e
.getMessage());
461 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
462 throw new Error
.GroupNotFound(e
.getMessage());
463 } catch (AttachmentInvalidException e
) {
464 throw new Error
.AttachmentInvalid(e
.getMessage());
469 public boolean isRegistered() {
474 public void updateProfile(
477 final String aboutEmoji
,
479 final boolean removeAvatar
482 if (avatarPath
.isEmpty()) {
485 Optional
<File
> avatarFile
= removeAvatar
487 : avatarPath
== null ?
null : Optional
.of(new File(avatarPath
));
488 m
.setProfile(name
, null, about
, aboutEmoji
, avatarFile
);
489 } catch (IOException e
) {
490 throw new Error
.Failure(e
.getMessage());
495 public void removePin() {
497 m
.setRegistrationLockPin(Optional
.absent());
498 } catch (UnauthenticatedResponseException e
) {
499 throw new Error
.Failure("Remove pin failed with unauthenticated response: " + e
.getMessage());
500 } catch (IOException e
) {
501 throw new Error
.Failure("Remove pin error: " + e
.getMessage());
506 public void setPin(String registrationLockPin
) {
508 m
.setRegistrationLockPin(Optional
.of(registrationLockPin
));
509 } catch (UnauthenticatedResponseException e
) {
510 throw new Error
.Failure("Set pin error failed with unauthenticated response: " + e
.getMessage());
511 } catch (IOException e
) {
512 throw new Error
.Failure("Set pin error: " + e
.getMessage());
516 // Provide option to query a version string in order to react on potential
517 // future interface changes
519 public String
version() {
520 return BaseConfig
.PROJECT_VERSION
;
523 // Create a unique list of Numbers from Identities and Contacts to really get
524 // all numbers the system knows
526 public List
<String
> listNumbers() {
527 return Stream
.concat(m
.getIdentities().stream().map(IdentityInfo
::getRecipientId
),
528 m
.getContacts().stream().map(Pair
::first
))
529 .map(m
::resolveSignalServiceAddress
)
530 .map(a
-> a
.getNumber().orNull())
531 .filter(Objects
::nonNull
)
533 .collect(Collectors
.toList());
537 public List
<String
> getContactNumber(final String name
) {
538 // Contact names have precedence.
539 var numbers
= new ArrayList
<String
>();
540 var contacts
= m
.getContacts();
541 for (var c
: contacts
) {
542 if (name
.equals(c
.second().getName())) {
543 numbers
.add(getLegacyIdentifier(m
.resolveSignalServiceAddress(c
.first())));
546 // Try profiles if no contact name was found
547 for (var identity
: m
.getIdentities()) {
548 final var recipientId
= identity
.getRecipientId();
549 final var address
= m
.resolveSignalServiceAddress(recipientId
);
550 var number
= address
.getNumber().orNull();
551 if (number
!= null) {
552 var profile
= m
.getRecipientProfile(recipientId
);
553 if (profile
!= null && profile
.getDisplayName().equals(name
)) {
562 public void quitGroup(final byte[] groupId
) {
563 var group
= getGroupId(groupId
);
565 m
.quitGroup(group
, Set
.of());
566 } catch (GroupNotFoundException
| NotAGroupMemberException e
) {
567 throw new Error
.GroupNotFound(e
.getMessage());
568 } catch (IOException
| LastGroupAdminException e
) {
569 throw new Error
.Failure(e
.getMessage());
574 public byte[] joinGroup(final String groupLink
) {
576 final var linkUrl
= GroupInviteLinkUrl
.fromUri(groupLink
);
577 if (linkUrl
== null) {
578 throw new Error
.Failure("Group link is invalid:");
580 final var result
= m
.joinGroup(linkUrl
);
581 return result
.first().serialize();
582 } catch (GroupInviteLinkUrl
.InvalidGroupLinkException
| GroupLinkNotActiveException e
) {
583 throw new Error
.Failure("Group link is invalid: " + e
.getMessage());
584 } catch (GroupInviteLinkUrl
.UnknownGroupLinkVersionException e
) {
585 throw new Error
.Failure("Group link was created with an incompatible version: " + e
.getMessage());
586 } catch (IOException e
) {
587 throw new Error
.Failure(e
.getMessage());
592 public boolean isContactBlocked(final String number
) {
593 return m
.isContactBlocked(getSingleRecipientIdentifier(number
, m
.getUsername()));
597 public boolean isGroupBlocked(final byte[] groupId
) {
598 var group
= m
.getGroup(getGroupId(groupId
));
602 return group
.isBlocked();
607 public boolean isMember(final byte[] groupId
) {
608 var group
= m
.getGroup(getGroupId(groupId
));
612 return group
.isMember(m
.getSelfRecipientId());
617 public String
uploadStickerPack(String stickerPackPath
) {
618 File path
= new File(stickerPackPath
);
620 return m
.uploadStickerPack(path
).toString();
621 } catch (IOException e
) {
622 throw new Error
.Failure("Upload error (maybe image size is too large):" + e
.getMessage());
623 } catch (StickerPackInvalidException e
) {
624 throw new Error
.Failure("Invalid sticker pack: " + e
.getMessage());
628 private static void checkSendMessageResult(long timestamp
, SendMessageResult result
) throws DBusExecutionException
{
629 var error
= ErrorUtils
.getErrorMessageFromSendMessageResult(result
);
635 final var message
= timestamp
+ "\nFailed to send message:\n" + error
+ '\n';
637 if (result
.getIdentityFailure() != null) {
638 throw new Error
.UntrustedIdentity(message
);
640 throw new Error
.Failure(message
);
644 private static void checkSendMessageResults(
645 long timestamp
, Map
<RecipientIdentifier
, List
<SendMessageResult
>> results
646 ) throws DBusExecutionException
{
647 final var sendMessageResults
= results
.values().stream().findFirst();
648 if (results
.size() == 1 && sendMessageResults
.get().size() == 1) {
649 checkSendMessageResult(timestamp
, sendMessageResults
.get().stream().findFirst().get());
653 var errors
= ErrorUtils
.getErrorMessagesFromSendMessageResults(results
);
654 if (errors
.size() == 0) {
658 var message
= new StringBuilder();
659 message
.append(timestamp
).append('\n');
660 message
.append("Failed to send (some) messages:\n");
661 for (var error
: errors
) {
662 message
.append(error
).append('\n');
665 throw new Error
.Failure(message
.toString());
668 private static void checkSendMessageResults(
669 long timestamp
, Collection
<SendMessageResult
> results
670 ) throws DBusExecutionException
{
671 if (results
.size() == 1) {
672 checkSendMessageResult(timestamp
, results
.stream().findFirst().get());
676 var errors
= ErrorUtils
.getErrorMessagesFromSendMessageResults(results
);
677 if (errors
.size() == 0) {
681 var message
= new StringBuilder();
682 message
.append(timestamp
).append('\n');
683 message
.append("Failed to send (some) messages:\n");
684 for (var error
: errors
) {
685 message
.append(error
).append('\n');
688 throw new Error
.Failure(message
.toString());
691 private static Set
<RecipientIdentifier
.Single
> getSingleRecipientIdentifiers(
692 final Collection
<String
> recipientStrings
, final String localNumber
693 ) throws DBusExecutionException
{
694 final var identifiers
= new HashSet
<RecipientIdentifier
.Single
>();
695 for (var recipientString
: recipientStrings
) {
696 identifiers
.add(getSingleRecipientIdentifier(recipientString
, localNumber
));
701 private static RecipientIdentifier
.Single
getSingleRecipientIdentifier(
702 final String recipientString
, final String localNumber
703 ) throws DBusExecutionException
{
705 return RecipientIdentifier
.Single
.fromString(recipientString
, localNumber
);
706 } catch (InvalidNumberException e
) {
707 throw new Error
.InvalidNumber(e
.getMessage());
711 private static GroupId
getGroupId(byte[] groupId
) throws DBusExecutionException
{
713 return GroupId
.unknownVersion(groupId
);
714 } catch (Throwable e
) {
715 throw new Error
.InvalidGroupId("Invalid group id: " + e
.getMessage());