X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/8bee08fd96571f0f08ffa713b7bd20a2b251d277..19b15e68e4ec652e23f9625662b7c0f96b963029:/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 4a478f13..1b8b7825 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -2,59 +2,129 @@ package org.asamk.signal.dbus; import org.asamk.Signal; import org.asamk.signal.BaseConfig; -import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.NotMasterDeviceException; -import org.asamk.signal.manager.StickerPackInvalidException; -import org.asamk.signal.manager.UntrustedIdentityException; +import org.asamk.signal.manager.api.AttachmentInvalidException; +import org.asamk.signal.manager.api.CaptchaRejectedException; +import org.asamk.signal.manager.api.DeviceLimitExceededException; +import org.asamk.signal.manager.api.DeviceLinkUrl; +import org.asamk.signal.manager.api.GroupId; +import org.asamk.signal.manager.api.GroupInviteLinkUrl; +import org.asamk.signal.manager.api.GroupLinkState; +import org.asamk.signal.manager.api.GroupNotFoundException; +import org.asamk.signal.manager.api.GroupPermission; +import org.asamk.signal.manager.api.GroupSendingNotAllowedException; +import org.asamk.signal.manager.api.IdentityVerificationCode; +import org.asamk.signal.manager.api.InactiveGroupLinkException; +import org.asamk.signal.manager.api.InvalidDeviceLinkException; +import org.asamk.signal.manager.api.InvalidNumberException; +import org.asamk.signal.manager.api.InvalidStickerException; +import org.asamk.signal.manager.api.LastGroupAdminException; import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.NotAGroupMemberException; +import org.asamk.signal.manager.api.NotPrimaryDeviceException; +import org.asamk.signal.manager.api.PendingAdminApprovalException; +import org.asamk.signal.manager.api.RateLimitException; +import org.asamk.signal.manager.api.RecipientAddress; import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.api.SendMessageResult; +import org.asamk.signal.manager.api.SendMessageResults; +import org.asamk.signal.manager.api.StickerPackInvalidException; import org.asamk.signal.manager.api.TypingAction; -import org.asamk.signal.manager.groups.GroupId; -import org.asamk.signal.manager.groups.GroupInviteLinkUrl; -import org.asamk.signal.manager.groups.GroupNotFoundException; -import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; -import org.asamk.signal.manager.groups.LastGroupAdminException; -import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.manager.storage.identities.IdentityInfo; -import org.asamk.signal.util.ErrorUtils; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.api.UnregisteredRecipientException; +import org.asamk.signal.manager.api.UpdateGroup; +import org.asamk.signal.manager.api.UpdateProfile; +import org.asamk.signal.manager.api.UserStatus; +import org.asamk.signal.util.DateUtils; +import org.asamk.signal.util.SendMessageResultUtils; +import org.freedesktop.dbus.DBusPath; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; -import org.whispersystems.libsignal.util.Pair; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; -import org.whispersystems.signalservice.api.messages.SendMessageResult; -import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; -import org.whispersystems.signalservice.api.util.InvalidNumberException; -import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; +import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.types.Variant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; -import java.util.stream.Stream; -import static org.asamk.signal.util.Util.getLegacyIdentifier; +import static org.asamk.signal.dbus.DbusUtils.makeValidObjectPathElement; -public class DbusSignalImpl implements Signal { +public class DbusSignalImpl implements Signal, AutoCloseable { private final Manager m; + private final DBusConnection connection; private final String objectPath; + private final boolean noReceiveOnStart; - public DbusSignalImpl(final Manager m, final String objectPath) { + private DBusPath thisDevice; + private final List devices = new ArrayList<>(); + private final List groups = new ArrayList<>(); + private final List identities = new ArrayList<>(); + private DbusReceiveMessageHandler dbusMessageHandler; + private int subscriberCount; + + private static final Logger logger = LoggerFactory.getLogger(DbusSignalImpl.class); + + public DbusSignalImpl( + final Manager m, DBusConnection connection, final String objectPath, final boolean noReceiveOnStart + ) { this.m = m; + this.connection = connection; this.objectPath = objectPath; + this.noReceiveOnStart = noReceiveOnStart; + + m.addAddressChangedListener(() -> { + unExportObjects(); + exportObjects(); + }); + } + + public void initObjects() { + exportObjects(); + if (!noReceiveOnStart) { + subscribeReceive(); + } + } + + private void exportObjects() { + exportObject(this); + + updateDevices(); + updateGroups(); + updateConfiguration(); + updateIdentities(); } @Override - public boolean isRemote() { - return false; + public void close() { + if (dbusMessageHandler != null) { + m.removeReceiveHandler(dbusMessageHandler); + dbusMessageHandler = null; + } + unExportObjects(); + } + + private void unExportObjects() { + unExportDevices(); + unExportGroups(); + unExportConfiguration(); + unExportIdentities(); + connection.unExportObject(this.objectPath); } @Override @@ -62,29 +132,128 @@ public class DbusSignalImpl implements Signal { return objectPath; } + @Override + public String getSelfNumber() { + return m.getSelfNumber(); + } + + @Override + public void subscribeReceive() { + if (dbusMessageHandler == null) { + dbusMessageHandler = new DbusReceiveMessageHandler(connection, objectPath); + m.addReceiveHandler(dbusMessageHandler); + } + subscriberCount++; + } + + @Override + public void unsubscribeReceive() { + subscriberCount = Math.max(0, subscriberCount - 1); + if (subscriberCount == 0 && dbusMessageHandler != null) { + m.removeReceiveHandler(dbusMessageHandler); + dbusMessageHandler = null; + } + } + + @Override + public void submitRateLimitChallenge(String challenge, String captcha) { + try { + m.submitRateLimitRecaptchaChallenge(challenge, captcha); + } catch (IOException e) { + throw new Error.Failure("Submit challenge error: " + e.getMessage()); + } catch (CaptchaRejectedException e) { + throw new Error.Failure( + "Captcha rejected, it may be outdated, already used or solved from a different IP address."); + } + } + + @Override + public void unregister() throws Error.Failure { + try { + m.unregister(); + } catch (IOException e) { + throw new Error.Failure("Failed to unregister: " + e.getMessage()); + } + } + + @Override + public void deleteAccount() throws Error.Failure { + try { + m.deleteAccount(); + } catch (IOException e) { + throw new Error.Failure("Failed to delete account: " + e.getMessage()); + } + } + + @Override + public void addDevice(String uri) { + try { + var deviceLinkUrl = DeviceLinkUrl.parseDeviceLinkUri(new URI(uri)); + m.addDeviceLink(deviceLinkUrl); + } catch (IOException | InvalidDeviceLinkException | DeviceLimitExceededException e) { + throw new Error.Failure(e.getClass().getSimpleName() + " Add device link failed. " + e.getMessage()); + } catch (NotPrimaryDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); + } catch (URISyntaxException e) { + throw new Error.InvalidUri(e.getClass().getSimpleName() + + " Device link uri has invalid format: " + + e.getMessage()); + } + } + + @Override + public DBusPath getDevice(long deviceId) { + updateDevices(); + final var deviceOptional = devices.stream().filter(g -> g.getId().equals(deviceId)).findFirst(); + if (deviceOptional.isEmpty()) { + throw new Error.DeviceNotFound("Device not found"); + } + return deviceOptional.get().getObjectPath(); + } + + @Override + public List listDevices() { + updateDevices(); + return this.devices; + } + + @Override + public DBusPath getThisDevice() { + updateDevices(); + return thisDevice; + } + @Override public long sendMessage(final String message, final List attachments, final String recipient) { - var recipients = new ArrayList(1); - recipients.add(recipient); - return sendMessage(message, attachments, recipients); + return sendMessage(message, attachments, List.of(recipient)); } @Override - public long sendMessage(final String message, final List attachments, final List recipients) { + public long sendMessage(final String messageText, final List attachments, final List recipients) { try { - final var results = m.sendMessage(new Message(message, attachments), - getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() - .map(RecipientIdentifier.class::cast) - .collect(Collectors.toSet())); + final var message = new Message(messageText, + attachments, + List.of(), + Optional.empty(), + Optional.empty(), + List.of(), + Optional.empty(), + List.of()); + final var recipientIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() + .map(RecipientIdentifier.class::cast) + .collect(Collectors.toSet()); + final var results = m.sendMessage(message, recipientIdentifiers, false); - checkSendMessageResults(results.getTimestamp(), results.getResults()); - return results.getTimestamp(); + checkSendMessageResults(results); + return results.timestamp(); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); - } catch (IOException e) { - throw new Error.Failure(e.getMessage()); + } catch (IOException | InvalidStickerException e) { + throw new Error.Failure(e); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); + } catch (UnregisteredRecipientException e) { + throw new Error.UntrustedIdentity(e.getSender().getIdentifier() + " is not registered."); } } @@ -92,9 +261,7 @@ public class DbusSignalImpl implements Signal { public long sendRemoteDeleteMessage( final long targetSentTimestamp, final String recipient ) { - var recipients = new ArrayList(1); - recipients.add(recipient); - return sendRemoteDeleteMessage(targetSentTimestamp, recipients); + return sendRemoteDeleteMessage(targetSentTimestamp, List.of(recipient)); } @Override @@ -103,27 +270,11 @@ public class DbusSignalImpl implements Signal { ) { try { final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, - getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); - checkSendMessageResults(results.getTimestamp(), results.getResults()); - return results.getTimestamp(); - } catch (IOException e) { - throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { - throw new Error.GroupNotFound(e.getMessage()); - } - } - - @Override - public long sendGroupRemoteDeleteMessage( - final long targetSentTimestamp, final byte[] groupId - ) { - try { - final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, - Set.of(new RecipientIdentifier.Group(getGroupId(groupId)))); - checkSendMessageResults(results.getTimestamp(), results.getResults()); - return results.getTimestamp(); + checkSendMessageResults(results); + return results.timestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { @@ -139,9 +290,7 @@ public class DbusSignalImpl implements Signal { final long targetSentTimestamp, final String recipient ) { - var recipients = new ArrayList(1); - recipients.add(recipient); - return sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients); + return sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, List.of(recipient)); } @Override @@ -155,17 +304,35 @@ public class DbusSignalImpl implements Signal { try { final var results = m.sendMessageReaction(emoji, remove, - getSingleRecipientIdentifier(targetAuthor, m.getUsername()), + getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()), targetSentTimestamp, - getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) - .collect(Collectors.toSet())); - checkSendMessageResults(results.getTimestamp(), results.getResults()); - return results.getTimestamp(); + .collect(Collectors.toSet()), + false); + checkSendMessageResults(results); + return results.timestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); + } catch (UnregisteredRecipientException e) { + throw new Error.UntrustedIdentity(e.getSender().getIdentifier() + " is not registered."); + } + } + + @Override + public long sendPaymentNotification( + final byte[] receipt, final String note, final String recipient + ) throws Error.Failure { + try { + final var results = m.sendPaymentNotificationMessage(receipt, + note, + getSingleRecipientIdentifier(recipient, m.getSelfNumber())); + checkSendMessageResults(results); + return results.timestamp(); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); } } @@ -174,32 +341,32 @@ public class DbusSignalImpl implements Signal { final String recipient, final boolean stop ) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity { try { - var recipients = new ArrayList(1); - recipients.add(recipient); - m.sendTypingMessage(stop ? TypingAction.STOP : TypingAction.START, - getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + final var results = m.sendTypingMessage(stop ? TypingAction.STOP : TypingAction.START, + getSingleRecipientIdentifiers(List.of(recipient), m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); + checkSendMessageResults(results); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); - } catch (UntrustedIdentityException e) { - throw new Error.UntrustedIdentity(e.getMessage()); } } @Override public void sendReadReceipt( - final String recipient, final List timestamps + final String recipient, final List messageIds ) throws Error.Failure, Error.UntrustedIdentity { - try { - m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getUsername()), timestamps); - } catch (IOException e) { - throw new Error.Failure(e.getMessage()); - } catch (UntrustedIdentityException e) { - throw new Error.UntrustedIdentity(e.getMessage()); - } + final var results = m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getSelfNumber()), messageIds); + checkSendMessageResults(results); + } + + @Override + public void sendViewedReceipt( + final String recipient, final List messageIds + ) throws Error.Failure, Error.UntrustedIdentity { + final var results = m.sendViewedReceipt(getSingleRecipientIdentifier(recipient, m.getSelfNumber()), messageIds); + checkSendMessageResults(results); } @Override @@ -222,45 +389,104 @@ public class DbusSignalImpl implements Signal { @Override public long sendNoteToSelfMessage( - final String message, final List attachments + final String messageText, final List attachments ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity { try { - final var results = m.sendMessage(new Message(message, attachments), - Set.of(new RecipientIdentifier.NoteToSelf())); - checkSendMessageResults(results.getTimestamp(), results.getResults()); - return results.getTimestamp(); + final var message = new Message(messageText, + attachments, + List.of(), + Optional.empty(), + Optional.empty(), + List.of(), + Optional.empty(), + List.of()); + final var results = m.sendMessage(message, Set.of(RecipientIdentifier.NoteToSelf.INSTANCE), false); + checkSendMessageResults(results); + return results.timestamp(); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); - } catch (IOException e) { + } catch (IOException | InvalidStickerException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); + } catch (UnregisteredRecipientException e) { + throw new Error.UntrustedIdentity(e.getSender().getIdentifier() + " is not registered."); } } @Override public void sendEndSessionMessage(final List recipients) { try { - final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getUsername())); - checkSendMessageResults(results.getTimestamp(), results.getResults()); + final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber())); + checkSendMessageResults(results); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } } @Override - public long sendGroupMessage(final String message, final List attachments, final byte[] groupId) { + public void deleteRecipient(final String recipient) throws Error.Failure { + m.deleteRecipient(getSingleRecipientIdentifier(recipient, m.getSelfNumber())); + } + + @Override + public void deleteContact(final String recipient) throws Error.Failure { + m.deleteContact(getSingleRecipientIdentifier(recipient, m.getSelfNumber())); + } + + @Override + public long sendGroupMessage(final String messageText, final List attachments, final byte[] groupId) { try { - var results = m.sendMessage(new Message(message, attachments), - Set.of(new RecipientIdentifier.Group(getGroupId(groupId)))); - checkSendMessageResults(results.getTimestamp(), results.getResults()); - return results.getTimestamp(); - } catch (IOException e) { + final var message = new Message(messageText, + attachments, + List.of(), + Optional.empty(), + Optional.empty(), + List.of(), + Optional.empty(), + List.of()); + var results = m.sendMessage(message, Set.of(getGroupRecipientIdentifier(groupId)), false); + checkSendMessageResults(results); + return results.timestamp(); + } catch (IOException | InvalidStickerException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); + } catch (UnregisteredRecipientException e) { + throw new Error.UntrustedIdentity(e.getSender().getIdentifier() + " is not registered."); + } + } + + @Override + public void sendGroupTyping( + final byte[] groupId, final boolean stop + ) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity { + try { + final var results = m.sendTypingMessage(stop ? TypingAction.STOP : TypingAction.START, + Set.of(getGroupRecipientIdentifier(groupId))); + checkSendMessageResults(results); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new Error.GroupNotFound(e.getMessage()); + } + } + + @Override + public long sendGroupRemoteDeleteMessage( + final long targetSentTimestamp, final byte[] groupId + ) { + try { + final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, + Set.of(getGroupRecipientIdentifier(groupId))); + checkSendMessageResults(results); + return results.timestamp(); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new Error.GroupNotFound(e.getMessage()); } } @@ -275,15 +501,18 @@ public class DbusSignalImpl implements Signal { try { final var results = m.sendMessageReaction(emoji, remove, - getSingleRecipientIdentifier(targetAuthor, m.getUsername()), + getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()), targetSentTimestamp, - Set.of(new RecipientIdentifier.Group(getGroupId(groupId)))); - checkSendMessageResults(results.getTimestamp(), results.getResults()); - return results.getTimestamp(); + Set.of(getGroupRecipientIdentifier(groupId)), + false); + checkSendMessageResults(results); + return results.timestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); + } catch (UnregisteredRecipientException e) { + throw new Error.UntrustedIdentity(e.getSender().getIdentifier() + " is not registered."); } } @@ -291,35 +520,52 @@ public class DbusSignalImpl implements Signal { // the profile name @Override public String getContactName(final String number) { - return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getUsername())); + final var name = m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getSelfNumber())); + return name == null ? "" : name; } @Override public void setContactName(final String number, final String name) { try { - m.setContactName(getSingleRecipientIdentifier(number, m.getUsername()), name); - } catch (NotMasterDeviceException e) { + m.setContactName(getSingleRecipientIdentifier(number, m.getSelfNumber()), name, ""); + } catch (NotPrimaryDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); - } catch (UnregisteredUserException e) { - throw new Error.Failure("Contact is not registered."); + } catch (UnregisteredRecipientException e) { + throw new Error.UntrustedIdentity(e.getSender().getIdentifier() + " is not registered."); + } + } + + @Override + public void setExpirationTimer(final String number, final int expiration) { + try { + m.setExpirationTimer(getSingleRecipientIdentifier(number, m.getSelfNumber()), expiration); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (UnregisteredRecipientException e) { + throw new Error.UntrustedIdentity(e.getSender().getIdentifier() + " is not registered."); } } @Override public void setContactBlocked(final String number, final boolean blocked) { try { - m.setContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()), blocked); - } catch (NotMasterDeviceException e) { + m.setContactsBlocked(List.of(getSingleRecipientIdentifier(number, m.getSelfNumber())), blocked); + } catch (NotPrimaryDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); } catch (IOException e) { throw new Error.Failure(e.getMessage()); + } catch (UnregisteredRecipientException e) { + throw new Error.UntrustedIdentity(e.getSender().getIdentifier() + " is not registered."); } } @Override + @Deprecated public void setGroupBlocked(final byte[] groupId, final boolean blocked) { try { - m.setGroupBlocked(getGroupId(groupId), blocked); + m.setGroupsBlocked(List.of(getGroupId(groupId)), blocked); + } catch (NotPrimaryDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (IOException e) { @@ -328,73 +574,84 @@ public class DbusSignalImpl implements Signal { } @Override + @Deprecated public List getGroupIds() { var groups = m.getGroups(); - var ids = new ArrayList(groups.size()); - for (var group : groups) { - ids.add(group.getGroupId().serialize()); + return groups.stream().map(g -> g.groupId().serialize()).toList(); + } + + @Override + public DBusPath getGroup(final byte[] groupId) { + updateGroups(); + final var groupOptional = groups.stream().filter(g -> Arrays.equals(g.getId(), groupId)).findFirst(); + if (groupOptional.isEmpty()) { + throw new Error.GroupNotFound("Group not found"); } - return ids; + return groupOptional.get().getObjectPath(); } @Override + public List listGroups() { + updateGroups(); + return groups; + } + + @Override + @Deprecated public String getGroupName(final byte[] groupId) { var group = m.getGroup(getGroupId(groupId)); - if (group == null) { + if (group == null || group.title() == null) { return ""; } else { - return group.getTitle(); + return group.title(); } } @Override + @Deprecated public List getGroupMembers(final byte[] groupId) { var group = m.getGroup(getGroupId(groupId)); if (group == null) { return List.of(); } else { - return group.getMembers() - .stream() - .map(m::resolveSignalServiceAddress) - .map(Util::getLegacyIdentifier) - .collect(Collectors.toList()); + final var members = group.members(); + return getRecipientStrings(members); } } @Override + public byte[] createGroup( + final String name, final List members, final String avatar + ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber { + return updateGroupInternal(new byte[0], name, members, avatar); + } + + @Override + @Deprecated public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) { + return updateGroupInternal(groupId, name, members, avatar); + } + + public byte[] updateGroupInternal(byte[] groupId, String name, List members, String avatar) { try { - if (groupId.length == 0) { - groupId = null; - } - if (name.isEmpty()) { - name = null; - } - if (avatar.isEmpty()) { - avatar = null; - } - final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getUsername()); + groupId = nullIfEmpty(groupId); + name = nullIfEmpty(name); + avatar = nullIfEmpty(avatar); + final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getSelfNumber()); if (groupId == null) { - final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar)); - checkSendMessageResults(results.second().getTimestamp(), results.second().getResults()); + final var results = m.createGroup(name, memberIdentifiers, avatar); + updateGroups(); + checkGroupSendMessageResults(results.second().timestamp(), results.second().results()); return results.first().serialize(); } else { final var results = m.updateGroup(getGroupId(groupId), - name, - null, - memberIdentifiers, - null, - null, - null, - false, - null, - null, - null, - avatar == null ? null : new File(avatar), - null, - null); + UpdateGroup.newBuilder() + .withName(name) + .withMembers(memberIdentifiers) + .withAvatarFile(avatar) + .build()); if (results != null) { - checkSendMessageResults(results.getTimestamp(), results.getResults()); + checkGroupSendMessageResults(results.timestamp(), results.results()); } return groupId; } @@ -404,43 +661,91 @@ public class DbusSignalImpl implements Signal { throw new Error.GroupNotFound(e.getMessage()); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); + } catch (UnregisteredRecipientException e) { + throw new Error.UntrustedIdentity(e.getSender().getIdentifier() + " is not registered."); } } @Override + @Deprecated public boolean isRegistered() { return true; } + @Override + public boolean isRegistered(String number) { + var result = isRegistered(List.of(number)); + return result.getFirst(); + } + + @Override + public List isRegistered(List numbers) { + if (numbers.isEmpty()) { + return List.of(); + } + + Map registered; + try { + registered = m.getUserStatus(new HashSet<>(numbers)); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (RateLimitException e) { + throw new Error.Failure(e.getMessage() + + ", retry at " + + DateUtils.formatTimestamp(e.getNextAttemptTimestamp())); + } + + return numbers.stream().map(number -> registered.get(number).uuid() != null).toList(); + } + @Override public void updateProfile( - final String name, - final String about, - final String aboutEmoji, + String givenName, + String familyName, + String about, + String aboutEmoji, String avatarPath, final boolean removeAvatar ) { try { - if (avatarPath.isEmpty()) { - avatarPath = null; - } - Optional avatarFile = removeAvatar - ? Optional.absent() - : avatarPath == null ? null : Optional.of(new File(avatarPath)); - m.setProfile(name, null, about, aboutEmoji, avatarFile); + givenName = nullIfEmpty(givenName); + familyName = nullIfEmpty(familyName); + about = nullIfEmpty(about); + aboutEmoji = nullIfEmpty(aboutEmoji); + avatarPath = nullIfEmpty(avatarPath); + final var avatarFile = removeAvatar || avatarPath == null ? null : avatarPath; + m.updateProfile(UpdateProfile.newBuilder() + .withGivenName(givenName) + .withFamilyName(familyName) + .withAbout(about) + .withAboutEmoji(aboutEmoji) + .withAvatar(avatarFile) + .withDeleteAvatar(removeAvatar) + .build()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } } + @Override + public void updateProfile( + final String name, + final String about, + final String aboutEmoji, + String avatarPath, + final boolean removeAvatar + ) { + updateProfile(name, "", about, aboutEmoji, avatarPath, removeAvatar); + } + @Override public void removePin() { try { - m.setRegistrationLockPin(Optional.absent()); - } catch (UnauthenticatedResponseException e) { - throw new Error.Failure("Remove pin failed with unauthenticated response: " + e.getMessage()); + m.setRegistrationLockPin(Optional.empty()); } catch (IOException e) { throw new Error.Failure("Remove pin error: " + e.getMessage()); + } catch (NotPrimaryDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); } } @@ -448,10 +753,10 @@ public class DbusSignalImpl implements Signal { public void setPin(String registrationLockPin) { try { m.setRegistrationLockPin(Optional.of(registrationLockPin)); - } catch (UnauthenticatedResponseException e) { - throw new Error.Failure("Set pin error failed with unauthenticated response: " + e.getMessage()); } catch (IOException e) { throw new Error.Failure("Set pin error: " + e.getMessage()); + } catch (NotPrimaryDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); } } @@ -466,41 +771,24 @@ public class DbusSignalImpl implements Signal { // all numbers the system knows @Override public List listNumbers() { - return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId), - m.getContacts().stream().map(Pair::first)) - .map(m::resolveSignalServiceAddress) - .map(a -> a.getNumber().orNull()) + return m.getRecipients(false, Optional.empty(), Set.of(), Optional.empty()) + .stream() + .map(r -> r.getAddress().number().orElse(null)) .filter(Objects::nonNull) .distinct() - .collect(Collectors.toList()); + .toList(); } @Override public List getContactNumber(final String name) { - // Contact names have precedence. - var numbers = new ArrayList(); - var contacts = m.getContacts(); - for (var c : contacts) { - if (name.equals(c.second().getName())) { - numbers.add(getLegacyIdentifier(m.resolveSignalServiceAddress(c.first()))); - } - } - // Try profiles if no contact name was found - for (var identity : m.getIdentities()) { - final var recipientId = identity.getRecipientId(); - final var address = m.resolveSignalServiceAddress(recipientId); - var number = address.getNumber().orNull(); - if (number != null) { - var profile = m.getRecipientProfile(recipientId); - if (profile != null && profile.getDisplayName().equals(name)) { - numbers.add(number); - } - } - } - return numbers; + return m.getRecipients(false, Optional.empty(), Set.of(), Optional.of(name)) + .stream() + .map(r -> r.getAddress().getLegacyIdentifier()) + .toList(); } @Override + @Deprecated public void quitGroup(final byte[] groupId) { var group = getGroupId(groupId); try { @@ -509,6 +797,8 @@ public class DbusSignalImpl implements Signal { throw new Error.GroupNotFound(e.getMessage()); } catch (IOException | LastGroupAdminException e) { throw new Error.Failure(e.getMessage()); + } catch (UnregisteredRecipientException e) { + throw new Error.UntrustedIdentity(e.getSender().getIdentifier() + " is not registered."); } } @@ -521,7 +811,9 @@ public class DbusSignalImpl implements Signal { } final var result = m.joinGroup(linkUrl); return result.first().serialize(); - } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupLinkNotActiveException e) { + } catch (PendingAdminApprovalException e) { + throw new Error.Failure("Pending admin approval: " + e.getMessage()); + } catch (GroupInviteLinkUrl.InvalidGroupLinkException | InactiveGroupLinkException e) { throw new Error.Failure("Group link is invalid: " + e.getMessage()); } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) { throw new Error.Failure("Group link was created with an incompatible version: " + e.getMessage()); @@ -532,10 +824,11 @@ public class DbusSignalImpl implements Signal { @Override public boolean isContactBlocked(final String number) { - return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getUsername())); + return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getSelfNumber())); } @Override + @Deprecated public boolean isGroupBlocked(final byte[] groupId) { var group = m.getGroup(getGroupId(groupId)); if (group == null) { @@ -546,12 +839,13 @@ public class DbusSignalImpl implements Signal { } @Override + @Deprecated public boolean isMember(final byte[] groupId) { var group = m.getGroup(getGroupId(groupId)); if (group == null) { return false; } else { - return group.isMember(m.getSelfRecipientId()); + return group.isMember(); } } @@ -568,46 +862,44 @@ public class DbusSignalImpl implements Signal { } private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException { - var error = ErrorUtils.getErrorMessageFromSendMessageResult(result); + var error = SendMessageResultUtils.getErrorMessageFromSendMessageResult(result); if (error == null) { return; } - final var message = timestamp + "\nFailed to send message:\n" + error + '\n'; + final var message = "\nFailed to send message:\n" + error + '\n' + timestamp; - if (result.getIdentityFailure() != null) { + if (result.isIdentityFailure()) { throw new Error.UntrustedIdentity(message); } else { throw new Error.Failure(message); } } - private static void checkSendMessageResults( - long timestamp, Map> results - ) throws DBusExecutionException { - final var sendMessageResults = results.values().stream().findFirst(); - if (results.size() == 1 && sendMessageResults.get().size() == 1) { - checkSendMessageResult(timestamp, sendMessageResults.get().stream().findFirst().get()); + private void checkSendMessageResults(final SendMessageResults results) { + final var sendMessageResults = results.results().values().stream().findFirst(); + if (results.results().size() == 1 && sendMessageResults.get().size() == 1) { + checkSendMessageResult(results.timestamp(), sendMessageResults.get().stream().findFirst().get()); return; } - var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results); - if (errors.size() == 0) { + if (results.hasSuccess()) { return; } var message = new StringBuilder(); - message.append(timestamp).append('\n'); - message.append("Failed to send (some) messages:\n"); + message.append("Failed to send messages:\n"); + var errors = SendMessageResultUtils.getErrorMessagesFromSendMessageResults(results.results()); for (var error : errors) { message.append(error).append('\n'); } + message.append(results.timestamp()); throw new Error.Failure(message.toString()); } - private static void checkSendMessageResults( + private static void checkGroupSendMessageResults( long timestamp, Collection results ) throws DBusExecutionException { if (results.size() == 1) { @@ -615,21 +907,25 @@ public class DbusSignalImpl implements Signal { return; } - var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results); - if (errors.size() == 0) { + var errors = SendMessageResultUtils.getErrorMessagesFromSendMessageResults(results); + if (errors.isEmpty() || errors.size() < results.size()) { return; } var message = new StringBuilder(); - message.append(timestamp).append('\n'); - message.append("Failed to send (some) messages:\n"); + message.append("Failed to send message:\n"); for (var error : errors) { message.append(error).append('\n'); } + message.append(timestamp); throw new Error.Failure(message.toString()); } + private static List getRecipientStrings(final Set members) { + return members.stream().map(RecipientAddress::getLegacyIdentifier).toList(); + } + private static Set getSingleRecipientIdentifiers( final Collection recipientStrings, final String localNumber ) throws DBusExecutionException { @@ -650,6 +946,10 @@ public class DbusSignalImpl implements Signal { } } + private RecipientIdentifier.Group getGroupRecipientIdentifier(final byte[] groupId) { + return new RecipientIdentifier.Group(getGroupId(groupId)); + } + private static GroupId getGroupId(byte[] groupId) throws DBusExecutionException { try { return GroupId.unknownVersion(groupId); @@ -657,4 +957,499 @@ public class DbusSignalImpl implements Signal { throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage()); } } + + private byte[] nullIfEmpty(final byte[] array) { + return array.length == 0 ? null : array; + } + + private String nullIfEmpty(final String name) { + return name.isEmpty() ? null : name; + } + + private String emptyIfNull(final String string) { + return string == null ? "" : string; + } + + private static String getDeviceObjectPath(String basePath, long deviceId) { + return basePath + "/Devices/" + deviceId; + } + + private void updateDevices() { + List linkedDevices; + try { + linkedDevices = m.getLinkedDevices(); + } catch (IOException e) { + throw new Error.Failure("Failed to get linked devices: " + e.getMessage()); + } + + unExportDevices(); + + linkedDevices.forEach(d -> { + final var object = new DbusSignalDeviceImpl(d); + final var deviceObjectPath = object.getObjectPath(); + exportObject(object); + if (d.isThisDevice()) { + thisDevice = new DBusPath(deviceObjectPath); + } + this.devices.add(new StructDevice(new DBusPath(deviceObjectPath), (long) d.id(), emptyIfNull(d.name()))); + }); + } + + private void unExportDevices() { + this.devices.stream() + .map(StructDevice::getObjectPath) + .map(DBusPath::getPath) + .forEach(connection::unExportObject); + this.devices.clear(); + } + + private static String getGroupObjectPath(String basePath, byte[] groupId) { + return basePath + "/Groups/" + makeValidObjectPathElement(Base64.getEncoder().encodeToString(groupId)); + } + + private void updateGroups() { + List groups; + groups = m.getGroups(); + + unExportGroups(); + + groups.forEach(g -> { + final var object = new DbusSignalGroupImpl(g.groupId()); + exportObject(object); + this.groups.add(new StructGroup(new DBusPath(object.getObjectPath()), + g.groupId().serialize(), + emptyIfNull(g.title()))); + }); + } + + private void unExportGroups() { + this.groups.stream().map(StructGroup::getObjectPath).map(DBusPath::getPath).forEach(connection::unExportObject); + this.groups.clear(); + } + + private static String getConfigurationObjectPath(String basePath) { + return basePath + "/Configuration"; + } + + private void updateConfiguration() { + unExportConfiguration(); + final var object = new DbusSignalConfigurationImpl(); + exportObject(object); + } + + private void unExportConfiguration() { + final var objectPath = getConfigurationObjectPath(this.objectPath); + connection.unExportObject(objectPath); + } + + private void exportObject(final DBusInterface object) { + try { + connection.exportObject(object); + logger.debug("Exported dbus object: " + object.getObjectPath()); + } catch (DBusException e) { + logger.warn("Failed to export dbus object (" + object.getObjectPath() + "): " + e.getMessage()); + } + } + + private void updateIdentities() { + List identities; + identities = m.getIdentities(); + + unExportIdentities(); + + identities.forEach(i -> { + final var object = new DbusSignalIdentityImpl(i); + exportObject(object); + this.identities.add(new StructIdentity(new DBusPath(object.getObjectPath()), + i.recipient().uuid().map(UUID::toString).orElse(""), + i.recipient().number().orElse(""))); + }); + } + + private static String getIdentityObjectPath(String basePath, String id) { + return basePath + "/Identities/" + makeValidObjectPathElement(id); + } + + private void unExportIdentities() { + this.identities.stream() + .map(StructIdentity::getObjectPath) + .map(DBusPath::getPath) + .forEach(connection::unExportObject); + this.identities.clear(); + } + + @Override + public DBusPath getIdentity(String number) throws Error.Failure { + final var found = identities.stream() + .filter(identity -> identity.getNumber().equals(number) || identity.getUuid().equals(number)) + .findFirst(); + + if (found.isEmpty()) { + throw new Error.Failure("Identity for " + number + " unknown"); + } + return found.get().getObjectPath(); + } + + @Override + public List listIdentities() { + updateIdentities(); + return this.identities; + } + + public class DbusSignalIdentityImpl extends DbusProperties implements Signal.Identity { + + private final org.asamk.signal.manager.api.Identity identity; + + public DbusSignalIdentityImpl(final org.asamk.signal.manager.api.Identity identity) { + this.identity = identity; + super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Identity", + List.of(new DbusProperty<>("Number", () -> identity.recipient().number().orElse("")), + new DbusProperty<>("Uuid", + () -> identity.recipient().uuid().map(UUID::toString).orElse("")), + new DbusProperty<>("Fingerprint", identity::fingerprint), + new DbusProperty<>("SafetyNumber", identity::safetyNumber), + new DbusProperty<>("ScannableSafetyNumber", identity::scannableSafetyNumber), + new DbusProperty<>("TrustLevel", identity::trustLevel), + new DbusProperty<>("AddedDate", identity::dateAddedTimestamp)))); + } + + @Override + public String getObjectPath() { + return getIdentityObjectPath(objectPath, + identity.recipient().getLegacyIdentifier() + "_" + identity.recipient().getIdentifier()); + } + + @Override + public void trust() throws Error.Failure { + var recipient = RecipientIdentifier.Single.fromAddress(identity.recipient()); + try { + m.trustIdentityAllKeys(recipient); + } catch (UnregisteredRecipientException e) { + throw new Error.Failure("The user " + e.getSender().getIdentifier() + " is not registered."); + } + updateIdentities(); + } + + @Override + public void trustVerified(String safetyNumber) throws Error.Failure { + var recipient = RecipientIdentifier.Single.fromAddress(identity.recipient()); + + if (safetyNumber == null) { + throw new Error.Failure("You need to specify a fingerprint/safety number"); + } + final IdentityVerificationCode verificationCode; + try { + verificationCode = IdentityVerificationCode.parse(safetyNumber); + } catch (Exception e) { + throw new Error.Failure( + "Safety number has invalid format, either specify the old hex fingerprint or the new safety number"); + } + + try { + final var res = m.trustIdentityVerified(recipient, verificationCode); + if (!res) { + throw new Error.Failure( + "Failed to set the trust for this number, make sure the number and the fingerprint/safety number are correct."); + } + } catch (UnregisteredRecipientException e) { + throw new Error.Failure("The user " + e.getSender().getIdentifier() + " is not registered."); + } + updateIdentities(); + } + } + + public class DbusSignalDeviceImpl extends DbusProperties implements Signal.Device { + + private final org.asamk.signal.manager.api.Device device; + + public DbusSignalDeviceImpl(final org.asamk.signal.manager.api.Device device) { + super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Device", + List.of(new DbusProperty<>("Id", device::id), + new DbusProperty<>("Name", () -> emptyIfNull(device.name()), this::setDeviceName), + new DbusProperty<>("Created", device::created), + new DbusProperty<>("LastSeen", device::lastSeen)))); + this.device = device; + } + + @Override + public String getObjectPath() { + return getDeviceObjectPath(objectPath, device.id()); + } + + @Override + public void removeDevice() throws Error.Failure { + try { + m.removeLinkedDevices(device.id()); + updateDevices(); + } catch (NotPrimaryDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + + private void setDeviceName(String name) { + if (!device.isThisDevice()) { + throw new Error.Failure("Only the name of this device can be changed"); + } + try { + m.updateAccountAttributes(name, null, null, null); + // update device list + updateDevices(); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + } + + public class DbusSignalConfigurationImpl extends DbusProperties implements Signal.Configuration { + + public DbusSignalConfigurationImpl() { + super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Configuration", + List.of(new DbusProperty<>("ReadReceipts", this::getReadReceipts, this::setReadReceipts), + new DbusProperty<>("UnidentifiedDeliveryIndicators", + this::getUnidentifiedDeliveryIndicators, + this::setUnidentifiedDeliveryIndicators), + new DbusProperty<>("TypingIndicators", + this::getTypingIndicators, + this::setTypingIndicators), + new DbusProperty<>("LinkPreviews", this::getLinkPreviews, this::setLinkPreviews)))); + + } + + @Override + public String getObjectPath() { + return getConfigurationObjectPath(objectPath); + } + + public void setReadReceipts(Boolean readReceipts) { + setConfiguration(readReceipts, null, null, null); + } + + public void setUnidentifiedDeliveryIndicators(Boolean unidentifiedDeliveryIndicators) { + setConfiguration(null, unidentifiedDeliveryIndicators, null, null); + } + + public void setTypingIndicators(Boolean typingIndicators) { + setConfiguration(null, null, typingIndicators, null); + } + + public void setLinkPreviews(Boolean linkPreviews) { + setConfiguration(null, null, null, linkPreviews); + } + + private void setConfiguration( + Boolean readReceipts, + Boolean unidentifiedDeliveryIndicators, + Boolean typingIndicators, + Boolean linkPreviews + ) { + try { + m.updateConfiguration(new org.asamk.signal.manager.api.Configuration(Optional.ofNullable(readReceipts), + Optional.ofNullable(unidentifiedDeliveryIndicators), + Optional.ofNullable(typingIndicators), + Optional.ofNullable(linkPreviews))); + } catch (NotPrimaryDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); + } + } + + private boolean getReadReceipts() { + return m.getConfiguration().readReceipts().orElse(false); + } + + private boolean getUnidentifiedDeliveryIndicators() { + return m.getConfiguration().unidentifiedDeliveryIndicators().orElse(false); + } + + private boolean getTypingIndicators() { + return m.getConfiguration().typingIndicators().orElse(false); + } + + private boolean getLinkPreviews() { + return m.getConfiguration().linkPreviews().orElse(false); + } + } + + public class DbusSignalGroupImpl extends DbusProperties implements Signal.Group { + + private final GroupId groupId; + + public DbusSignalGroupImpl(final GroupId groupId) { + this.groupId = groupId; + super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Group", + List.of(new DbusProperty<>("Id", groupId::serialize), + new DbusProperty<>("Name", () -> emptyIfNull(getGroup().title()), this::setGroupName), + new DbusProperty<>("Description", + () -> emptyIfNull(getGroup().description()), + this::setGroupDescription), + new DbusProperty<>("Avatar", this::setGroupAvatar), + new DbusProperty<>("IsBlocked", () -> getGroup().isBlocked(), this::setIsBlocked), + new DbusProperty<>("IsMember", () -> getGroup().isMember()), + new DbusProperty<>("IsAdmin", () -> getGroup().isAdmin()), + new DbusProperty<>("MessageExpirationTimer", + () -> getGroup().messageExpirationTimer(), + this::setMessageExpirationTime), + new DbusProperty<>("Members", + () -> new Variant<>(getRecipientStrings(getGroup().members()), "as")), + new DbusProperty<>("PendingMembers", + () -> new Variant<>(getRecipientStrings(getGroup().pendingMembers()), "as")), + new DbusProperty<>("RequestingMembers", + () -> new Variant<>(getRecipientStrings(getGroup().requestingMembers()), "as")), + new DbusProperty<>("Admins", + () -> new Variant<>(getRecipientStrings(getGroup().adminMembers()), "as")), + new DbusProperty<>("Banned", + () -> new Variant<>(getRecipientStrings(getGroup().bannedMembers()), "as")), + new DbusProperty<>("PermissionAddMember", + () -> getGroup().permissionAddMember().name(), + this::setGroupPermissionAddMember), + new DbusProperty<>("PermissionEditDetails", + () -> getGroup().permissionEditDetails().name(), + this::setGroupPermissionEditDetails), + new DbusProperty<>("PermissionSendMessage", + () -> getGroup().permissionSendMessage().name(), + this::setGroupPermissionSendMessage), + new DbusProperty<>("GroupInviteLink", () -> { + final var groupInviteLinkUrl = getGroup().groupInviteLinkUrl(); + return groupInviteLinkUrl == null ? "" : groupInviteLinkUrl.getUrl(); + })))); + } + + @Override + public String getObjectPath() { + return getGroupObjectPath(objectPath, groupId.serialize()); + } + + @Override + public void quitGroup() throws Error.Failure { + try { + m.quitGroup(groupId, Set.of()); + } catch (GroupNotFoundException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (NotAGroupMemberException e) { + throw new Error.NotAGroupMember(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (LastGroupAdminException e) { + throw new Error.LastGroupAdmin(e.getMessage()); + } catch (UnregisteredRecipientException e) { + throw new Error.UntrustedIdentity(e.getSender().getIdentifier() + " is not registered."); + } + } + + @Override + public void deleteGroup() throws Error.Failure, Error.LastGroupAdmin { + try { + m.deleteGroup(groupId); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + updateGroups(); + } + + @Override + public void addMembers(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withMembers(memberIdentifiers).build()); + } + + @Override + public void removeMembers(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withRemoveMembers(memberIdentifiers).build()); + } + + @Override + public void addAdmins(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withAdmins(memberIdentifiers).build()); + } + + @Override + public void removeAdmins(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withRemoveAdmins(memberIdentifiers).build()); + } + + @Override + public void resetLink() throws Error.Failure { + updateGroup(UpdateGroup.newBuilder().withResetGroupLink(true).build()); + } + + @Override + public void disableLink() throws Error.Failure { + updateGroup(UpdateGroup.newBuilder().withGroupLinkState(GroupLinkState.DISABLED).build()); + } + + @Override + public void enableLink(final boolean requiresApproval) throws Error.Failure { + updateGroup(UpdateGroup.newBuilder() + .withGroupLinkState(requiresApproval + ? GroupLinkState.ENABLED_WITH_APPROVAL + : GroupLinkState.ENABLED) + .build()); + } + + private org.asamk.signal.manager.api.Group getGroup() { + return m.getGroup(groupId); + } + + private void setGroupName(final String name) { + updateGroup(UpdateGroup.newBuilder().withName(name).build()); + } + + private void setGroupDescription(final String description) { + updateGroup(UpdateGroup.newBuilder().withDescription(description).build()); + } + + private void setGroupAvatar(final String avatar) { + updateGroup(UpdateGroup.newBuilder().withAvatarFile(avatar).build()); + } + + private void setMessageExpirationTime(final int expirationTime) { + updateGroup(UpdateGroup.newBuilder().withExpirationTimer(expirationTime).build()); + } + + private void setGroupPermissionAddMember(final String permission) { + updateGroup(UpdateGroup.newBuilder().withAddMemberPermission(GroupPermission.valueOf(permission)).build()); + } + + private void setGroupPermissionEditDetails(final String permission) { + updateGroup(UpdateGroup.newBuilder() + .withEditDetailsPermission(GroupPermission.valueOf(permission)) + .build()); + } + + private void setGroupPermissionSendMessage(final String permission) { + updateGroup(UpdateGroup.newBuilder() + .withIsAnnouncementGroup(GroupPermission.valueOf(permission) == GroupPermission.ONLY_ADMINS) + .build()); + } + + private void setIsBlocked(final boolean isBlocked) { + try { + m.setGroupsBlocked(List.of(groupId), isBlocked); + } catch (NotPrimaryDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); + } catch (GroupNotFoundException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + + private void updateGroup(final UpdateGroup updateGroup) { + try { + m.updateGroup(groupId, updateGroup); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (AttachmentInvalidException e) { + throw new Error.AttachmentInvalid(e.getMessage()); + } catch (UnregisteredRecipientException e) { + throw new Error.UntrustedIdentity(e.getSender().getIdentifier() + " is not registered."); + } + } + } }