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
.Identity
;
11 import org
.asamk
.signal
.manager
.api
.InactiveGroupLinkException
;
12 import org
.asamk
.signal
.manager
.api
.InvalidDeviceLinkException
;
13 import org
.asamk
.signal
.manager
.api
.InvalidNumberException
;
14 import org
.asamk
.signal
.manager
.api
.Message
;
15 import org
.asamk
.signal
.manager
.api
.Pair
;
16 import org
.asamk
.signal
.manager
.api
.RecipientIdentifier
;
17 import org
.asamk
.signal
.manager
.api
.SendMessageResult
;
18 import org
.asamk
.signal
.manager
.api
.TypingAction
;
19 import org
.asamk
.signal
.manager
.api
.UpdateGroup
;
20 import org
.asamk
.signal
.manager
.groups
.GroupId
;
21 import org
.asamk
.signal
.manager
.groups
.GroupInviteLinkUrl
;
22 import org
.asamk
.signal
.manager
.groups
.GroupLinkState
;
23 import org
.asamk
.signal
.manager
.groups
.GroupNotFoundException
;
24 import org
.asamk
.signal
.manager
.groups
.GroupPermission
;
25 import org
.asamk
.signal
.manager
.groups
.GroupSendingNotAllowedException
;
26 import org
.asamk
.signal
.manager
.groups
.LastGroupAdminException
;
27 import org
.asamk
.signal
.manager
.groups
.NotAGroupMemberException
;
28 import org
.asamk
.signal
.manager
.storage
.recipients
.Profile
;
29 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientAddress
;
30 import org
.asamk
.signal
.util
.ErrorUtils
;
31 import org
.freedesktop
.dbus
.DBusPath
;
32 import org
.freedesktop
.dbus
.connections
.impl
.DBusConnection
;
33 import org
.freedesktop
.dbus
.exceptions
.DBusException
;
34 import org
.freedesktop
.dbus
.exceptions
.DBusExecutionException
;
35 import org
.freedesktop
.dbus
.types
.Variant
;
38 import java
.io
.IOException
;
40 import java
.net
.URISyntaxException
;
41 import java
.util
.ArrayList
;
42 import java
.util
.Arrays
;
43 import java
.util
.Base64
;
44 import java
.util
.Collection
;
45 import java
.util
.HashSet
;
46 import java
.util
.List
;
48 import java
.util
.Objects
;
49 import java
.util
.Optional
;
51 import java
.util
.UUID
;
52 import java
.util
.stream
.Collectors
;
53 import java
.util
.stream
.Stream
;
55 public class DbusSignalImpl
implements Signal
{
57 private final Manager m
;
58 private final DBusConnection connection
;
59 private final String objectPath
;
61 private DBusPath thisDevice
;
62 private final List
<StructDevice
> devices
= new ArrayList
<>();
63 private final List
<StructGroup
> groups
= new ArrayList
<>();
65 public DbusSignalImpl(final Manager m
, DBusConnection connection
, final String objectPath
) {
67 this.connection
= connection
;
68 this.objectPath
= objectPath
;
71 public void initObjects() {
82 public String
getObjectPath() {
87 public String
getSelfNumber() {
88 return m
.getSelfNumber();
92 public void submitRateLimitChallenge(String challenge
, String captchaString
) {
93 final var captcha
= captchaString
== null ?
null : captchaString
.replace("signalcaptcha://", "");
96 m
.submitRateLimitRecaptchaChallenge(challenge
, captcha
);
97 } catch (IOException e
) {
98 throw new Error
.Failure("Submit challenge error: " + e
.getMessage());
104 public void addDevice(String uri
) {
106 m
.addDeviceLink(new URI(uri
));
107 } catch (IOException
| InvalidDeviceLinkException e
) {
108 throw new Error
.Failure(e
.getClass().getSimpleName() + " Add device link failed. " + e
.getMessage());
109 } catch (URISyntaxException e
) {
110 throw new Error
.InvalidUri(e
.getClass().getSimpleName()
111 + " Device link uri has invalid format: "
117 public DBusPath
getDevice(long deviceId
) {
119 final var deviceOptional
= devices
.stream().filter(g
-> g
.getId().equals(deviceId
)).findFirst();
120 if (deviceOptional
.isEmpty()) {
121 throw new Error
.DeviceNotFound("Device not found");
123 return deviceOptional
.get().getObjectPath();
127 public List
<StructDevice
> listDevices() {
133 public DBusPath
getThisDevice() {
139 public long sendMessage(final String message
, final List
<String
> attachments
, final String recipient
) {
140 var recipients
= new ArrayList
<String
>(1);
141 recipients
.add(recipient
);
142 return sendMessage(message
, attachments
, recipients
);
146 public long sendMessage(final String message
, final List
<String
> attachments
, final List
<String
> recipients
) {
148 final var results
= m
.sendMessage(new Message(message
, attachments
),
149 getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber()).stream()
150 .map(RecipientIdentifier
.class::cast
)
151 .collect(Collectors
.toSet()));
153 checkSendMessageResults(results
.timestamp(), results
.results());
154 return results
.timestamp();
155 } catch (AttachmentInvalidException e
) {
156 throw new Error
.AttachmentInvalid(e
.getMessage());
157 } catch (IOException e
) {
158 throw new Error
.Failure(e
);
159 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
160 throw new Error
.GroupNotFound(e
.getMessage());
165 public long sendRemoteDeleteMessage(
166 final long targetSentTimestamp
, final String recipient
168 var recipients
= new ArrayList
<String
>(1);
169 recipients
.add(recipient
);
170 return sendRemoteDeleteMessage(targetSentTimestamp
, recipients
);
174 public long sendRemoteDeleteMessage(
175 final long targetSentTimestamp
, final List
<String
> recipients
178 final var results
= m
.sendRemoteDeleteMessage(targetSentTimestamp
,
179 getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber()).stream()
180 .map(RecipientIdentifier
.class::cast
)
181 .collect(Collectors
.toSet()));
182 checkSendMessageResults(results
.timestamp(), results
.results());
183 return results
.timestamp();
184 } catch (IOException e
) {
185 throw new Error
.Failure(e
.getMessage());
186 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
187 throw new Error
.GroupNotFound(e
.getMessage());
192 public long sendGroupRemoteDeleteMessage(
193 final long targetSentTimestamp
, final byte[] groupId
196 final var results
= m
.sendRemoteDeleteMessage(targetSentTimestamp
,
197 Set
.of(new RecipientIdentifier
.Group(getGroupId(groupId
))));
198 checkSendMessageResults(results
.timestamp(), results
.results());
199 return results
.timestamp();
200 } catch (IOException e
) {
201 throw new Error
.Failure(e
.getMessage());
202 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
203 throw new Error
.GroupNotFound(e
.getMessage());
208 public long sendMessageReaction(
210 final boolean remove
,
211 final String targetAuthor
,
212 final long targetSentTimestamp
,
213 final String recipient
215 var recipients
= new ArrayList
<String
>(1);
216 recipients
.add(recipient
);
217 return sendMessageReaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
, recipients
);
221 public long sendMessageReaction(
223 final boolean remove
,
224 final String targetAuthor
,
225 final long targetSentTimestamp
,
226 final List
<String
> recipients
229 final var results
= m
.sendMessageReaction(emoji
,
231 getSingleRecipientIdentifier(targetAuthor
, m
.getSelfNumber()),
233 getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber()).stream()
234 .map(RecipientIdentifier
.class::cast
)
235 .collect(Collectors
.toSet()));
236 checkSendMessageResults(results
.timestamp(), results
.results());
237 return results
.timestamp();
238 } catch (IOException e
) {
239 throw new Error
.Failure(e
.getMessage());
240 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
241 throw new Error
.GroupNotFound(e
.getMessage());
246 public void sendTyping(
247 final String recipient
, final boolean stop
248 ) throws Error
.Failure
, Error
.GroupNotFound
, Error
.UntrustedIdentity
{
250 var recipients
= new ArrayList
<String
>(1);
251 recipients
.add(recipient
);
252 m
.sendTypingMessage(stop ? TypingAction
.STOP
: TypingAction
.START
,
253 getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber()).stream()
254 .map(RecipientIdentifier
.class::cast
)
255 .collect(Collectors
.toSet()));
256 } catch (IOException e
) {
257 throw new Error
.Failure(e
.getMessage());
258 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
259 throw new Error
.GroupNotFound(e
.getMessage());
260 } catch (UntrustedIdentityException e
) {
261 throw new Error
.UntrustedIdentity(e
.getMessage());
266 public void sendReadReceipt(
267 final String recipient
, final List
<Long
> messageIds
268 ) throws Error
.Failure
, Error
.UntrustedIdentity
{
270 m
.sendReadReceipt(getSingleRecipientIdentifier(recipient
, m
.getSelfNumber()), messageIds
);
271 } catch (IOException e
) {
272 throw new Error
.Failure(e
.getMessage());
273 } catch (UntrustedIdentityException e
) {
274 throw new Error
.UntrustedIdentity(e
.getMessage());
279 public void sendViewedReceipt(
280 final String recipient
, final List
<Long
> messageIds
281 ) throws Error
.Failure
, Error
.UntrustedIdentity
{
283 m
.sendViewedReceipt(getSingleRecipientIdentifier(recipient
, m
.getSelfNumber()), messageIds
);
284 } catch (IOException e
) {
285 throw new Error
.Failure(e
.getMessage());
286 } catch (UntrustedIdentityException e
) {
287 throw new Error
.UntrustedIdentity(e
.getMessage());
292 public void sendContacts() {
295 } catch (IOException e
) {
296 throw new Error
.Failure("SendContacts error: " + e
.getMessage());
301 public void sendSyncRequest() {
303 m
.requestAllSyncData();
304 } catch (IOException e
) {
305 throw new Error
.Failure("Request sync data error: " + e
.getMessage());
310 public long sendNoteToSelfMessage(
311 final String message
, final List
<String
> attachments
312 ) throws Error
.AttachmentInvalid
, Error
.Failure
, Error
.UntrustedIdentity
{
314 final var results
= m
.sendMessage(new Message(message
, attachments
),
315 Set
.of(RecipientIdentifier
.NoteToSelf
.INSTANCE
));
316 checkSendMessageResults(results
.timestamp(), results
.results());
317 return results
.timestamp();
318 } catch (AttachmentInvalidException e
) {
319 throw new Error
.AttachmentInvalid(e
.getMessage());
320 } catch (IOException e
) {
321 throw new Error
.Failure(e
.getMessage());
322 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
323 throw new Error
.GroupNotFound(e
.getMessage());
328 public void sendEndSessionMessage(final List
<String
> recipients
) {
330 final var results
= m
.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber()));
331 checkSendMessageResults(results
.timestamp(), results
.results());
332 } catch (IOException e
) {
333 throw new Error
.Failure(e
.getMessage());
338 public long sendGroupMessage(final String message
, final List
<String
> attachments
, final byte[] groupId
) {
340 var results
= m
.sendMessage(new Message(message
, attachments
),
341 Set
.of(new RecipientIdentifier
.Group(getGroupId(groupId
))));
342 checkSendMessageResults(results
.timestamp(), results
.results());
343 return results
.timestamp();
344 } catch (IOException e
) {
345 throw new Error
.Failure(e
.getMessage());
346 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
347 throw new Error
.GroupNotFound(e
.getMessage());
348 } catch (AttachmentInvalidException e
) {
349 throw new Error
.AttachmentInvalid(e
.getMessage());
354 public long sendGroupMessageReaction(
356 final boolean remove
,
357 final String targetAuthor
,
358 final long targetSentTimestamp
,
362 final var results
= m
.sendMessageReaction(emoji
,
364 getSingleRecipientIdentifier(targetAuthor
, m
.getSelfNumber()),
366 Set
.of(new RecipientIdentifier
.Group(getGroupId(groupId
))));
367 checkSendMessageResults(results
.timestamp(), results
.results());
368 return results
.timestamp();
369 } catch (IOException e
) {
370 throw new Error
.Failure(e
.getMessage());
371 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
372 throw new Error
.GroupNotFound(e
.getMessage());
376 // Since contact names might be empty if not defined, also potentially return
379 public String
getContactName(final String number
) {
380 final var name
= m
.getContactOrProfileName(getSingleRecipientIdentifier(number
, m
.getSelfNumber()));
381 return name
== null ?
"" : name
;
385 public void setContactName(final String number
, final String name
) {
387 m
.setContactName(getSingleRecipientIdentifier(number
, m
.getSelfNumber()), name
);
388 } catch (NotMasterDeviceException e
) {
389 throw new Error
.Failure("This command doesn't work on linked devices.");
390 } catch (IOException e
) {
391 throw new Error
.Failure("Contact is not registered.");
396 public void setExpirationTimer(final String number
, final int expiration
) {
398 m
.setExpirationTimer(getSingleRecipientIdentifier(number
, m
.getSelfNumber()), expiration
);
399 } catch (IOException e
) {
400 throw new Error
.Failure(e
.getMessage());
405 public void setContactBlocked(final String number
, final boolean blocked
) {
407 m
.setContactBlocked(getSingleRecipientIdentifier(number
, m
.getSelfNumber()), blocked
);
408 } catch (NotMasterDeviceException e
) {
409 throw new Error
.Failure("This command doesn't work on linked devices.");
410 } catch (IOException e
) {
411 throw new Error
.Failure(e
.getMessage());
416 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) {
418 m
.setGroupBlocked(getGroupId(groupId
), blocked
);
419 } catch (NotMasterDeviceException e
) {
420 throw new Error
.Failure("This command doesn't work on linked devices.");
421 } catch (GroupNotFoundException e
) {
422 throw new Error
.GroupNotFound(e
.getMessage());
423 } catch (IOException e
) {
424 throw new Error
.Failure(e
.getMessage());
429 public List
<byte[]> getGroupIds() {
430 var groups
= m
.getGroups();
431 var ids
= new ArrayList
<byte[]>(groups
.size());
432 for (var group
: groups
) {
433 ids
.add(group
.groupId().serialize());
439 public DBusPath
getGroup(final byte[] groupId
) {
441 final var groupOptional
= groups
.stream().filter(g
-> Arrays
.equals(g
.getId(), groupId
)).findFirst();
442 if (groupOptional
.isEmpty()) {
443 throw new Error
.GroupNotFound("Group not found");
445 return groupOptional
.get().getObjectPath();
449 public List
<StructGroup
> listGroups() {
455 public String
getGroupName(final byte[] groupId
) {
456 var group
= m
.getGroup(getGroupId(groupId
));
457 if (group
== null || group
.title() == null) {
460 return group
.title();
465 public List
<String
> getGroupMembers(final byte[] groupId
) {
466 var group
= m
.getGroup(getGroupId(groupId
));
470 final var members
= group
.members();
471 return getRecipientStrings(members
);
476 public byte[] createGroup(
477 final String name
, final List
<String
> members
, final String avatar
478 ) throws Error
.AttachmentInvalid
, Error
.Failure
, Error
.InvalidNumber
{
479 return updateGroup(new byte[0], name
, members
, avatar
);
483 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) {
485 groupId
= nullIfEmpty(groupId
);
486 name
= nullIfEmpty(name
);
487 avatar
= nullIfEmpty(avatar
);
488 final var memberIdentifiers
= getSingleRecipientIdentifiers(members
, m
.getSelfNumber());
489 if (groupId
== null) {
490 final var results
= m
.createGroup(name
, memberIdentifiers
, avatar
== null ?
null : new File(avatar
));
491 checkSendMessageResults(results
.second().timestamp(), results
.second().results());
492 return results
.first().serialize();
494 final var results
= m
.updateGroup(getGroupId(groupId
),
495 UpdateGroup
.newBuilder()
497 .withMembers(memberIdentifiers
)
498 .withAvatarFile(avatar
== null ?
null : new File(avatar
))
500 if (results
!= null) {
501 checkSendMessageResults(results
.timestamp(), results
.results());
505 } catch (IOException e
) {
506 throw new Error
.Failure(e
.getMessage());
507 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
508 throw new Error
.GroupNotFound(e
.getMessage());
509 } catch (AttachmentInvalidException e
) {
510 throw new Error
.AttachmentInvalid(e
.getMessage());
515 public boolean isRegistered() {
520 public boolean isRegistered(String number
) {
521 var result
= isRegistered(List
.of(number
));
522 return result
.get(0);
526 public List
<Boolean
> isRegistered(List
<String
> numbers
) {
527 var results
= new ArrayList
<Boolean
>();
528 if (numbers
.isEmpty()) {
532 Map
<String
, Pair
<String
, UUID
>> registered
;
534 registered
= m
.areUsersRegistered(new HashSet
<>(numbers
));
535 } catch (IOException e
) {
536 throw new Error
.Failure(e
.getMessage());
539 return numbers
.stream().map(number
-> {
540 var uuid
= registered
.get(number
).second();
542 }).collect(Collectors
.toList());
546 public void updateProfile(
552 final boolean removeAvatar
555 givenName
= nullIfEmpty(givenName
);
556 familyName
= nullIfEmpty(familyName
);
557 about
= nullIfEmpty(about
);
558 aboutEmoji
= nullIfEmpty(aboutEmoji
);
559 avatarPath
= nullIfEmpty(avatarPath
);
560 Optional
<File
> avatarFile
= removeAvatar
562 : avatarPath
== null ?
null : Optional
.of(new File(avatarPath
));
563 m
.setProfile(givenName
, familyName
, about
, aboutEmoji
, avatarFile
);
564 } catch (IOException e
) {
565 throw new Error
.Failure(e
.getMessage());
570 public void updateProfile(
573 final String aboutEmoji
,
575 final boolean removeAvatar
577 updateProfile(name
, "", about
, aboutEmoji
, avatarPath
, removeAvatar
);
581 public void removePin() {
583 m
.setRegistrationLockPin(Optional
.empty());
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 (IOException e
) {
594 throw new Error
.Failure("Set pin error: " + e
.getMessage());
598 // Provide option to query a version string in order to react on potential
599 // future interface changes
601 public String
version() {
602 return BaseConfig
.PROJECT_VERSION
;
605 // Create a unique list of Numbers from Identities and Contacts to really get
606 // all numbers the system knows
608 public List
<String
> listNumbers() {
609 return Stream
.concat(m
.getIdentities().stream().map(Identity
::recipient
),
610 m
.getContacts().stream().map(Pair
::first
))
611 .map(a
-> a
.getNumber().orElse(null))
612 .filter(Objects
::nonNull
)
614 .collect(Collectors
.toList());
618 public List
<String
> getContactNumber(final String name
) {
619 // Contact names have precedence.
620 var numbers
= new ArrayList
<String
>();
621 var contacts
= m
.getContacts();
622 for (var c
: contacts
) {
623 if (name
.equals(c
.second().getName())) {
624 numbers
.add(c
.first().getLegacyIdentifier());
627 // Try profiles if no contact name was found
628 for (var identity
: m
.getIdentities()) {
629 final var address
= identity
.recipient();
630 var number
= address
.getNumber().orElse(null);
631 if (number
!= null) {
632 Profile profile
= null;
634 profile
= m
.getRecipientProfile(RecipientIdentifier
.Single
.fromAddress(address
));
635 } catch (IOException ignored
) {
637 if (profile
!= null && profile
.getDisplayName().equals(name
)) {
646 public void quitGroup(final byte[] groupId
) {
647 var group
= getGroupId(groupId
);
649 m
.quitGroup(group
, Set
.of());
650 } catch (GroupNotFoundException
| NotAGroupMemberException e
) {
651 throw new Error
.GroupNotFound(e
.getMessage());
652 } catch (IOException
| LastGroupAdminException e
) {
653 throw new Error
.Failure(e
.getMessage());
658 public byte[] joinGroup(final String groupLink
) {
660 final var linkUrl
= GroupInviteLinkUrl
.fromUri(groupLink
);
661 if (linkUrl
== null) {
662 throw new Error
.Failure("Group link is invalid:");
664 final var result
= m
.joinGroup(linkUrl
);
665 return result
.first().serialize();
666 } catch (GroupInviteLinkUrl
.InvalidGroupLinkException
| InactiveGroupLinkException e
) {
667 throw new Error
.Failure("Group link is invalid: " + e
.getMessage());
668 } catch (GroupInviteLinkUrl
.UnknownGroupLinkVersionException e
) {
669 throw new Error
.Failure("Group link was created with an incompatible version: " + e
.getMessage());
670 } catch (IOException e
) {
671 throw new Error
.Failure(e
.getMessage());
676 public boolean isContactBlocked(final String number
) {
677 return m
.isContactBlocked(getSingleRecipientIdentifier(number
, m
.getSelfNumber()));
681 public boolean isGroupBlocked(final byte[] groupId
) {
682 var group
= m
.getGroup(getGroupId(groupId
));
686 return group
.isBlocked();
691 public boolean isMember(final byte[] groupId
) {
692 var group
= m
.getGroup(getGroupId(groupId
));
696 return group
.isMember();
701 public String
uploadStickerPack(String stickerPackPath
) {
702 File path
= new File(stickerPackPath
);
704 return m
.uploadStickerPack(path
).toString();
705 } catch (IOException e
) {
706 throw new Error
.Failure("Upload error (maybe image size is too large):" + e
.getMessage());
707 } catch (StickerPackInvalidException e
) {
708 throw new Error
.Failure("Invalid sticker pack: " + e
.getMessage());
712 private static void checkSendMessageResult(long timestamp
, SendMessageResult result
) throws DBusExecutionException
{
713 var error
= ErrorUtils
.getErrorMessageFromSendMessageResult(result
);
719 final var message
= timestamp
+ "\nFailed to send message:\n" + error
+ '\n';
721 if (result
.isIdentityFailure()) {
722 throw new Error
.UntrustedIdentity(message
);
724 throw new Error
.Failure(message
);
728 private static void checkSendMessageResults(
729 long timestamp
, Map
<RecipientIdentifier
, List
<SendMessageResult
>> results
730 ) throws DBusExecutionException
{
731 final var sendMessageResults
= results
.values().stream().findFirst();
732 if (results
.size() == 1 && sendMessageResults
.get().size() == 1) {
733 checkSendMessageResult(timestamp
, sendMessageResults
.get().stream().findFirst().get());
737 var errors
= ErrorUtils
.getErrorMessagesFromSendMessageResults(results
);
738 if (errors
.size() == 0) {
742 var message
= new StringBuilder();
743 message
.append(timestamp
).append('\n');
744 message
.append("Failed to send (some) messages:\n");
745 for (var error
: errors
) {
746 message
.append(error
).append('\n');
749 throw new Error
.Failure(message
.toString());
752 private static void checkSendMessageResults(
753 long timestamp
, Collection
<SendMessageResult
> results
754 ) throws DBusExecutionException
{
755 if (results
.size() == 1) {
756 checkSendMessageResult(timestamp
, results
.stream().findFirst().get());
760 var errors
= ErrorUtils
.getErrorMessagesFromSendMessageResults(results
);
761 if (errors
.size() == 0) {
765 var message
= new StringBuilder();
766 message
.append(timestamp
).append('\n');
767 message
.append("Failed to send (some) messages:\n");
768 for (var error
: errors
) {
769 message
.append(error
).append('\n');
772 throw new Error
.Failure(message
.toString());
775 private static List
<String
> getRecipientStrings(final Set
<RecipientAddress
> members
) {
776 return members
.stream().map(RecipientAddress
::getLegacyIdentifier
).collect(Collectors
.toList());
779 private static Set
<RecipientIdentifier
.Single
> getSingleRecipientIdentifiers(
780 final Collection
<String
> recipientStrings
, final String localNumber
781 ) throws DBusExecutionException
{
782 final var identifiers
= new HashSet
<RecipientIdentifier
.Single
>();
783 for (var recipientString
: recipientStrings
) {
784 identifiers
.add(getSingleRecipientIdentifier(recipientString
, localNumber
));
789 private static RecipientIdentifier
.Single
getSingleRecipientIdentifier(
790 final String recipientString
, final String localNumber
791 ) throws DBusExecutionException
{
793 return RecipientIdentifier
.Single
.fromString(recipientString
, localNumber
);
794 } catch (InvalidNumberException e
) {
795 throw new Error
.InvalidNumber(e
.getMessage());
799 private static GroupId
getGroupId(byte[] groupId
) throws DBusExecutionException
{
801 return GroupId
.unknownVersion(groupId
);
802 } catch (Throwable e
) {
803 throw new Error
.InvalidGroupId("Invalid group id: " + e
.getMessage());
807 private byte[] nullIfEmpty(final byte[] array
) {
808 return array
.length
== 0 ?
null : array
;
811 private String
nullIfEmpty(final String name
) {
812 return name
.isEmpty() ?
null : name
;
815 private String
emptyIfNull(final String string
) {
816 return string
== null ?
"" : string
;
819 private static String
getDeviceObjectPath(String basePath
, long deviceId
) {
820 return basePath
+ "/Devices/" + deviceId
;
823 private void updateDevices() {
824 List
<org
.asamk
.signal
.manager
.api
.Device
> linkedDevices
;
826 linkedDevices
= m
.getLinkedDevices();
827 } catch (IOException e
) {
828 throw new Error
.Failure("Failed to get linked devices: " + e
.getMessage());
833 linkedDevices
.forEach(d
-> {
834 final var object
= new DbusSignalDeviceImpl(d
);
835 final var deviceObjectPath
= object
.getObjectPath();
837 connection
.exportObject(object
);
838 } catch (DBusException e
) {
841 if (d
.isThisDevice()) {
842 thisDevice
= new DBusPath(deviceObjectPath
);
844 this.devices
.add(new StructDevice(new DBusPath(deviceObjectPath
), d
.id(), emptyIfNull(d
.name())));
848 private void unExportDevices() {
849 this.devices
.stream()
850 .map(StructDevice
::getObjectPath
)
851 .map(DBusPath
::getPath
)
852 .forEach(connection
::unExportObject
);
853 this.devices
.clear();
856 private static String
getGroupObjectPath(String basePath
, byte[] groupId
) {
857 return basePath
+ "/Groups/" + Base64
.getEncoder()
858 .encodeToString(groupId
)
864 private void updateGroups() {
865 List
<org
.asamk
.signal
.manager
.api
.Group
> groups
;
866 groups
= m
.getGroups();
870 groups
.forEach(g
-> {
871 final var object
= new DbusSignalGroupImpl(g
.groupId());
873 connection
.exportObject(object
);
874 } catch (DBusException e
) {
877 this.groups
.add(new StructGroup(new DBusPath(object
.getObjectPath()),
878 g
.groupId().serialize(),
879 emptyIfNull(g
.title())));
883 private void unExportGroups() {
884 this.groups
.stream().map(StructGroup
::getObjectPath
).map(DBusPath
::getPath
).forEach(connection
::unExportObject
);
888 public class DbusSignalDeviceImpl
extends DbusProperties
implements Signal
.Device
{
890 private final org
.asamk
.signal
.manager
.api
.Device device
;
892 public DbusSignalDeviceImpl(final org
.asamk
.signal
.manager
.api
.Device device
) {
893 super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Device",
894 List
.of(new DbusProperty
<>("Id", device
::id
),
895 new DbusProperty
<>("Name", () -> emptyIfNull(device
.name()), this::setDeviceName
),
896 new DbusProperty
<>("Created", device
::created
),
897 new DbusProperty
<>("LastSeen", device
::lastSeen
))));
898 this.device
= device
;
902 public String
getObjectPath() {
903 return getDeviceObjectPath(objectPath
, device
.id());
907 public void removeDevice() throws Error
.Failure
{
909 m
.removeLinkedDevices(device
.id());
911 } catch (IOException e
) {
912 throw new Error
.Failure(e
.getMessage());
916 private void setDeviceName(String name
) {
917 if (!device
.isThisDevice()) {
918 throw new Error
.Failure("Only the name of this device can be changed");
921 m
.updateAccountAttributes(name
);
922 // update device list
924 } catch (IOException e
) {
925 throw new Error
.Failure(e
.getMessage());
930 public class DbusSignalGroupImpl
extends DbusProperties
implements Signal
.Group
{
932 private final GroupId groupId
;
934 public DbusSignalGroupImpl(final GroupId groupId
) {
935 this.groupId
= groupId
;
936 super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Group",
937 List
.of(new DbusProperty
<>("Id", groupId
::serialize
),
938 new DbusProperty
<>("Name", () -> emptyIfNull(getGroup().title()), this::setGroupName
),
939 new DbusProperty
<>("Description",
940 () -> emptyIfNull(getGroup().description()),
941 this::setGroupDescription
),
942 new DbusProperty
<>("Avatar", this::setGroupAvatar
),
943 new DbusProperty
<>("IsBlocked", () -> getGroup().isBlocked(), this::setIsBlocked
),
944 new DbusProperty
<>("IsMember", () -> getGroup().isMember()),
945 new DbusProperty
<>("IsAdmin", () -> getGroup().isAdmin()),
946 new DbusProperty
<>("MessageExpirationTimer",
947 () -> getGroup().messageExpirationTimer(),
948 this::setMessageExpirationTime
),
949 new DbusProperty
<>("Members",
950 () -> new Variant
<>(getRecipientStrings(getGroup().members()), "as")),
951 new DbusProperty
<>("PendingMembers",
952 () -> new Variant
<>(getRecipientStrings(getGroup().pendingMembers()), "as")),
953 new DbusProperty
<>("RequestingMembers",
954 () -> new Variant
<>(getRecipientStrings(getGroup().requestingMembers()), "as")),
955 new DbusProperty
<>("Admins",
956 () -> new Variant
<>(getRecipientStrings(getGroup().adminMembers()), "as")),
957 new DbusProperty
<>("PermissionAddMember",
958 () -> getGroup().permissionAddMember().name(),
959 this::setGroupPermissionAddMember
),
960 new DbusProperty
<>("PermissionEditDetails",
961 () -> getGroup().permissionEditDetails().name(),
962 this::setGroupPermissionEditDetails
),
963 new DbusProperty
<>("PermissionSendMessage",
964 () -> getGroup().permissionSendMessage().name(),
965 this::setGroupPermissionSendMessage
),
966 new DbusProperty
<>("GroupInviteLink", () -> {
967 final var groupInviteLinkUrl
= getGroup().groupInviteLinkUrl();
968 return groupInviteLinkUrl
== null ?
"" : groupInviteLinkUrl
.getUrl();
973 public String
getObjectPath() {
974 return getGroupObjectPath(objectPath
, groupId
.serialize());
978 public void quitGroup() throws Error
.Failure
{
980 m
.quitGroup(groupId
, Set
.of());
981 } catch (GroupNotFoundException
| NotAGroupMemberException e
) {
982 throw new Error
.GroupNotFound(e
.getMessage());
983 } catch (IOException e
) {
984 throw new Error
.Failure(e
.getMessage());
985 } catch (LastGroupAdminException e
) {
986 throw new Error
.LastGroupAdmin(e
.getMessage());
991 public void addMembers(final List
<String
> recipients
) throws Error
.Failure
{
992 final var memberIdentifiers
= getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber());
993 updateGroup(UpdateGroup
.newBuilder().withMembers(memberIdentifiers
).build());
997 public void removeMembers(final List
<String
> recipients
) throws Error
.Failure
{
998 final var memberIdentifiers
= getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber());
999 updateGroup(UpdateGroup
.newBuilder().withRemoveMembers(memberIdentifiers
).build());
1003 public void addAdmins(final List
<String
> recipients
) throws Error
.Failure
{
1004 final var memberIdentifiers
= getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber());
1005 updateGroup(UpdateGroup
.newBuilder().withAdmins(memberIdentifiers
).build());
1009 public void removeAdmins(final List
<String
> recipients
) throws Error
.Failure
{
1010 final var memberIdentifiers
= getSingleRecipientIdentifiers(recipients
, m
.getSelfNumber());
1011 updateGroup(UpdateGroup
.newBuilder().withRemoveAdmins(memberIdentifiers
).build());
1015 public void resetLink() throws Error
.Failure
{
1016 updateGroup(UpdateGroup
.newBuilder().withResetGroupLink(true).build());
1020 public void disableLink() throws Error
.Failure
{
1021 updateGroup(UpdateGroup
.newBuilder().withGroupLinkState(GroupLinkState
.DISABLED
).build());
1025 public void enableLink(final boolean requiresApproval
) throws Error
.Failure
{
1026 updateGroup(UpdateGroup
.newBuilder()
1027 .withGroupLinkState(requiresApproval
1028 ? GroupLinkState
.ENABLED_WITH_APPROVAL
1029 : GroupLinkState
.ENABLED
)
1033 private org
.asamk
.signal
.manager
.api
.Group
getGroup() {
1034 return m
.getGroup(groupId
);
1037 private void setGroupName(final String name
) {
1038 updateGroup(UpdateGroup
.newBuilder().withName(name
).build());
1041 private void setGroupDescription(final String description
) {
1042 updateGroup(UpdateGroup
.newBuilder().withDescription(description
).build());
1045 private void setGroupAvatar(final String avatar
) {
1046 updateGroup(UpdateGroup
.newBuilder().withAvatarFile(new File(avatar
)).build());
1049 private void setMessageExpirationTime(final int expirationTime
) {
1050 updateGroup(UpdateGroup
.newBuilder().withExpirationTimer(expirationTime
).build());
1053 private void setGroupPermissionAddMember(final String permission
) {
1054 updateGroup(UpdateGroup
.newBuilder().withAddMemberPermission(GroupPermission
.valueOf(permission
)).build());
1057 private void setGroupPermissionEditDetails(final String permission
) {
1058 updateGroup(UpdateGroup
.newBuilder()
1059 .withEditDetailsPermission(GroupPermission
.valueOf(permission
))
1063 private void setGroupPermissionSendMessage(final String permission
) {
1064 updateGroup(UpdateGroup
.newBuilder()
1065 .withIsAnnouncementGroup(GroupPermission
.valueOf(permission
) == GroupPermission
.ONLY_ADMINS
)
1069 private void setIsBlocked(final boolean isBlocked
) {
1071 m
.setGroupBlocked(groupId
, isBlocked
);
1072 } catch (NotMasterDeviceException e
) {
1073 throw new Error
.Failure("This command doesn't work on linked devices.");
1074 } catch (GroupNotFoundException e
) {
1075 throw new Error
.GroupNotFound(e
.getMessage());
1076 } catch (IOException e
) {
1077 throw new Error
.Failure(e
.getMessage());
1081 private void updateGroup(final UpdateGroup updateGroup
) {
1083 m
.updateGroup(groupId
, updateGroup
);
1084 } catch (IOException e
) {
1085 throw new Error
.Failure(e
.getMessage());
1086 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
1087 throw new Error
.GroupNotFound(e
.getMessage());
1088 } catch (AttachmentInvalidException e
) {
1089 throw new Error
.AttachmentInvalid(e
.getMessage());