]> nmode's Git Repositories - signal-cli/commitdiff
Merge branch master into dbus_updateConfiguration
authorJohn Freed <okgithub@johnfreed.com>
Thu, 14 Oct 2021 13:24:07 +0000 (15:24 +0200)
committerJohn Freed <okgithub@johnfreed.com>
Thu, 14 Oct 2021 13:24:07 +0000 (15:24 +0200)
1  2 
lib/src/main/java/org/asamk/signal/manager/Manager.java
lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
man/signal-cli-dbus.5.adoc
src/main/java/org/asamk/Signal.java
src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java

index 943b5e7580526dfbe84dc147ddb18e1c6513acb1,733e3dccc285fe831e966e03c646ef5c207fa1c2..93afd9a691d59ca2970fde2b021bd37c434d5fde
@@@ -8,13 -8,12 +8,12 @@@ import org.asamk.signal.manager.api.Rec
  import org.asamk.signal.manager.api.SendGroupMessageResults;
  import org.asamk.signal.manager.api.SendMessageResults;
  import org.asamk.signal.manager.api.TypingAction;
+ import org.asamk.signal.manager.api.UpdateGroup;
  import org.asamk.signal.manager.config.ServiceConfig;
  import org.asamk.signal.manager.config.ServiceEnvironment;
  import org.asamk.signal.manager.groups.GroupId;
  import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
- import org.asamk.signal.manager.groups.GroupLinkState;
  import org.asamk.signal.manager.groups.GroupNotFoundException;
- import org.asamk.signal.manager.groups.GroupPermission;
  import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
  import org.asamk.signal.manager.groups.LastGroupAdminException;
  import org.asamk.signal.manager.groups.NotAGroupMemberException;
@@@ -23,7 -22,6 +22,6 @@@ import org.asamk.signal.manager.storage
  import org.asamk.signal.manager.storage.recipients.Contact;
  import org.asamk.signal.manager.storage.recipients.Profile;
  import org.asamk.signal.manager.storage.recipients.RecipientAddress;
- import org.whispersystems.libsignal.IdentityKey;
  import org.whispersystems.libsignal.InvalidKeyException;
  import org.whispersystems.libsignal.util.Pair;
  import org.whispersystems.libsignal.util.guava.Optional;
@@@ -105,8 -103,6 +103,8 @@@ public interface Manager extends Closea
              final Boolean linkPreviews
      ) throws IOException, NotMasterDeviceException;
  
 +    List<Boolean> getConfiguration() throws IOException, NotMasterDeviceException;
 +
      void setProfile(
              String givenName, String familyName, String about, String aboutEmoji, Optional<File> avatar
      ) throws IOException;
      ) throws IOException, AttachmentInvalidException;
  
      SendGroupMessageResults updateGroup(
-             GroupId groupId,
-             String name,
-             String description,
-             Set<RecipientIdentifier.Single> members,
-             Set<RecipientIdentifier.Single> removeMembers,
-             Set<RecipientIdentifier.Single> admins,
-             Set<RecipientIdentifier.Single> removeAdmins,
-             boolean resetGroupLink,
-             GroupLinkState groupLinkState,
-             GroupPermission addMemberPermission,
-             GroupPermission editDetailsPermission,
-             File avatarFile,
-             Integer expirationTimer,
-             Boolean isAnnouncementGroup
+             final GroupId groupId, final UpdateGroup updateGroup
      ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException;
  
      Pair<GroupId, SendGroupMessageResults> joinGroup(
  
      void setGroupBlocked(
              GroupId groupId, boolean blocked
-     ) throws GroupNotFoundException, IOException;
+     ) throws GroupNotFoundException, IOException, NotMasterDeviceException;
  
      void setExpirationTimer(
              RecipientIdentifier.Single recipient, int messageExpirationTimer
  
      boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient);
  
-     String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey);
      SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address);
  
      @Override
index b8414329d77ccc14b7037a8ca7ef658d3faa4541,d2ffaaabe577d8c6d2a7bf749a24fe70712ce457..a4121e29279e44e5befe13dd47e577647d34126e
@@@ -25,13 -25,12 +25,12 @@@ import org.asamk.signal.manager.api.Rec
  import org.asamk.signal.manager.api.SendGroupMessageResults;
  import org.asamk.signal.manager.api.SendMessageResults;
  import org.asamk.signal.manager.api.TypingAction;
+ import org.asamk.signal.manager.api.UpdateGroup;
  import org.asamk.signal.manager.config.ServiceConfig;
  import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
  import org.asamk.signal.manager.groups.GroupId;
  import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
- import org.asamk.signal.manager.groups.GroupLinkState;
  import org.asamk.signal.manager.groups.GroupNotFoundException;
- import org.asamk.signal.manager.groups.GroupPermission;
  import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
  import org.asamk.signal.manager.groups.LastGroupAdminException;
  import org.asamk.signal.manager.groups.NotAGroupMemberException;
@@@ -39,6 -38,7 +38,7 @@@ import org.asamk.signal.manager.helper.
  import org.asamk.signal.manager.helper.ContactHelper;
  import org.asamk.signal.manager.helper.GroupHelper;
  import org.asamk.signal.manager.helper.GroupV2Helper;
+ import org.asamk.signal.manager.helper.IdentityHelper;
  import org.asamk.signal.manager.helper.IncomingMessageHandler;
  import org.asamk.signal.manager.helper.PinHelper;
  import org.asamk.signal.manager.helper.PreKeyHelper;
@@@ -60,15 -60,10 +60,10 @@@ import org.asamk.signal.manager.storage
  import org.asamk.signal.manager.storage.stickers.StickerPackId;
  import org.asamk.signal.manager.util.KeyUtils;
  import org.asamk.signal.manager.util.StickerUtils;
- import org.asamk.signal.manager.util.Utils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
- import org.whispersystems.libsignal.IdentityKey;
  import org.whispersystems.libsignal.InvalidKeyException;
  import org.whispersystems.libsignal.ecc.ECPublicKey;
- import org.whispersystems.libsignal.fingerprint.Fingerprint;
- import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
- import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
  import org.whispersystems.libsignal.util.Pair;
  import org.whispersystems.libsignal.util.guava.Optional;
  import org.whispersystems.signalservice.api.SignalSessionLock;
@@@ -99,9 -94,7 +94,7 @@@ import java.net.URISyntaxException
  import java.net.URLEncoder;
  import java.nio.charset.StandardCharsets;
  import java.security.SignatureException;
- import java.util.Arrays;
  import java.util.Collection;
- import java.util.Date;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
@@@ -113,7 -106,6 +106,6 @@@ import java.util.concurrent.Executors
  import java.util.concurrent.TimeUnit;
  import java.util.concurrent.TimeoutException;
  import java.util.concurrent.locks.ReentrantLock;
- import java.util.function.Function;
  import java.util.stream.Collectors;
  
  import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
@@@ -139,6 -131,7 +131,7 @@@ public class ManagerImpl implements Man
      private final ContactHelper contactHelper;
      private final IncomingMessageHandler incomingMessageHandler;
      private final PreKeyHelper preKeyHelper;
+     private final IdentityHelper identityHelper;
  
      private final Context context;
      private boolean hasCaughtUpWithOldMessages = false;
  
          this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore);
          this.pinHelper = new PinHelper(dependencies.getKeyBackupService());
-         final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
-                 account.getProfileStore()::getProfileKey,
-                 this::getRecipientProfile,
-                 this::getSenderCertificate);
+         final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account,
+                 dependencies,
+                 account::getProfileKey,
+                 this::getRecipientProfile);
          this.profileHelper = new ProfileHelper(account,
                  dependencies,
                  avatarStore,
-                 account.getProfileStore()::getProfileKey,
                  unidentifiedAccessHelper::getAccessFor,
                  this::resolveSignalServiceAddress);
          final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential,
                  syncHelper,
                  this::getRecipientProfile,
                  jobExecutor);
+         this.identityHelper = new IdentityHelper(account,
+                 dependencies,
+                 this::resolveSignalServiceAddress,
+                 syncHelper,
+                 profileHelper);
      }
  
      @Override
          syncHelper.sendConfigurationMessage();
      }
  
 +    @Override
 +    public List<Boolean> getConfiguration() throws IOException, NotMasterDeviceException {
 +        if (!account.isMasterDevice()) {
 +            throw new NotMasterDeviceException();
 +        }
 +        final var configurationStore = account.getConfigurationStore();
 +        final Boolean readReceipts = configurationStore.getReadReceipts();
 +        final Boolean unidentifiedDeliveryIndicators = configurationStore.getUnidentifiedDeliveryIndicators();
 +        final Boolean typingIndicators = configurationStore.getTypingIndicators();
 +        final Boolean linkPreviews = configurationStore.getLinkPreviews();
 +        return List.of(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews);
 +    }
 +
      /**
       * @param givenName  if null, the previous givenName will be kept
       * @param familyName if null, the previous familyName will be kept
                          .map(account.getRecipientStore()::resolveRecipientAddress)
                          .collect(Collectors.toSet()),
                  groupInfo.isBlocked(),
-                 groupInfo.getMessageExpirationTime(),
-                 groupInfo.isAnnouncementGroup(),
-                 groupInfo.isMember(account.getSelfRecipientId()));
+                 groupInfo.getMessageExpirationTimer(),
+                 groupInfo.getPermissionAddMember(),
+                 groupInfo.getPermissionEditDetails(),
+                 groupInfo.getPermissionSendMessage(),
+                 groupInfo.isMember(account.getSelfRecipientId()),
+                 groupInfo.isAdmin(account.getSelfRecipientId()));
      }
  
      @Override
  
      @Override
      public SendGroupMessageResults updateGroup(
-             GroupId groupId,
-             String name,
-             String description,
-             Set<RecipientIdentifier.Single> members,
-             Set<RecipientIdentifier.Single> removeMembers,
-             Set<RecipientIdentifier.Single> admins,
-             Set<RecipientIdentifier.Single> removeAdmins,
-             boolean resetGroupLink,
-             GroupLinkState groupLinkState,
-             GroupPermission addMemberPermission,
-             GroupPermission editDetailsPermission,
-             File avatarFile,
-             Integer expirationTimer,
-             Boolean isAnnouncementGroup
+             final GroupId groupId, final UpdateGroup updateGroup
      ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
          return groupHelper.updateGroup(groupId,
-                 name,
-                 description,
-                 members == null ? null : resolveRecipients(members),
-                 removeMembers == null ? null : resolveRecipients(removeMembers),
-                 admins == null ? null : resolveRecipients(admins),
-                 removeAdmins == null ? null : resolveRecipients(removeAdmins),
-                 resetGroupLink,
-                 groupLinkState,
-                 addMemberPermission,
-                 editDetailsPermission,
-                 avatarFile,
-                 expirationTimer,
-                 isAnnouncementGroup);
+                 updateGroup.getName(),
+                 updateGroup.getDescription(),
+                 updateGroup.getMembers() == null ? null : resolveRecipients(updateGroup.getMembers()),
+                 updateGroup.getRemoveMembers() == null ? null : resolveRecipients(updateGroup.getRemoveMembers()),
+                 updateGroup.getAdmins() == null ? null : resolveRecipients(updateGroup.getAdmins()),
+                 updateGroup.getRemoveAdmins() == null ? null : resolveRecipients(updateGroup.getRemoveAdmins()),
+                 updateGroup.isResetGroupLink(),
+                 updateGroup.getGroupLinkState(),
+                 updateGroup.getAddMemberPermission(),
+                 updateGroup.getEditDetailsPermission(),
+                 updateGroup.getAvatarFile(),
+                 updateGroup.getExpirationTimer(),
+                 updateGroup.getIsAnnouncementGroup());
      }
  
      @Override
      @Override
      public void setGroupBlocked(
              final GroupId groupId, final boolean blocked
-     ) throws GroupNotFoundException, IOException {
+     ) throws GroupNotFoundException, IOException, NotMasterDeviceException {
+         if (!account.isMasterDevice()) {
+             throw new NotMasterDeviceException();
+         }
          groupHelper.setGroupBlocked(groupId, blocked);
          // TODO cycle our profile key
          syncHelper.sendBlockedList();
          }
      }
  
-     private byte[] getSenderCertificate() {
-         byte[] certificate;
-         try {
-             if (account.isPhoneNumberShared()) {
-                 certificate = dependencies.getAccountManager().getSenderCertificate();
-             } else {
-                 certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy();
-             }
-         } catch (IOException e) {
-             logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
-             return null;
-         }
-         // TODO cache for a day
-         return certificate;
-     }
      private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException {
          final var address = resolveSignalServiceAddress(recipientId);
          if (!address.getNumber().isPresent()) {
          return toGroup(groupHelper.getGroup(groupId));
      }
  
-     public GroupInfo getGroupInfo(GroupId groupId) {
+     private GroupInfo getGroupInfo(GroupId groupId) {
          return groupHelper.getGroup(groupId);
      }
  
          final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId());
          return new Identity(address,
                  identityInfo.getIdentityKey(),
-                 computeSafetyNumber(address.toSignalServiceAddress(), identityInfo.getIdentityKey()),
-                 computeSafetyNumberForScanning(address.toSignalServiceAddress(), identityInfo.getIdentityKey()),
+                 identityHelper.computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()),
+                 identityHelper.computeSafetyNumberForScanning(identityInfo.getRecipientId(),
+                         identityInfo.getIdentityKey()).getSerialized(),
                  identityInfo.getTrustLevel(),
                  identityInfo.getDateAdded());
      }
          } catch (UnregisteredUserException e) {
              return false;
          }
-         return trustIdentity(recipientId,
-                 identityKey -> Arrays.equals(identityKey.serialize(), fingerprint),
-                 TrustLevel.TRUSTED_VERIFIED);
+         return identityHelper.trustIdentityVerified(recipientId, fingerprint);
      }
  
      /**
          } catch (UnregisteredUserException e) {
              return false;
          }
-         var address = resolveSignalServiceAddress(recipientId);
-         return trustIdentity(recipientId,
-                 identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)),
-                 TrustLevel.TRUSTED_VERIFIED);
+         return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber);
      }
  
      /**
          } catch (UnregisteredUserException e) {
              return false;
          }
-         var address = resolveSignalServiceAddress(recipientId);
-         return trustIdentity(recipientId, identityKey -> {
-             final var fingerprint = computeSafetyNumberFingerprint(address, identityKey);
-             try {
-                 return fingerprint != null && fingerprint.getScannableFingerprint().compareTo(safetyNumber);
-             } catch (FingerprintVersionMismatchException | FingerprintParsingException e) {
-                 return false;
-             }
-         }, TrustLevel.TRUSTED_VERIFIED);
+         return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber);
      }
  
      /**
          } catch (UnregisteredUserException e) {
              return false;
          }
-         return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED);
-     }
-     private boolean trustIdentity(
-             RecipientId recipientId, Function<IdentityKey, Boolean> verifier, TrustLevel trustLevel
-     ) {
-         var identity = account.getIdentityKeyStore().getIdentity(recipientId);
-         if (identity == null) {
-             return false;
-         }
-         if (!verifier.apply(identity.getIdentityKey())) {
-             return false;
-         }
-         account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel);
-         try {
-             var address = resolveSignalServiceAddress(recipientId);
-             syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel);
-         } catch (IOException e) {
-             logger.warn("Failed to send verification sync message: {}", e.getMessage());
-         }
-         return true;
+         return identityHelper.trustIdentityAllKeys(recipientId);
      }
  
      private void handleIdentityFailure(
              final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure
      ) {
-         final var identityKey = identityFailure.getIdentityKey();
-         if (identityKey != null) {
-             final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date());
-             if (newIdentity) {
-                 account.getSessionStore().archiveSessions(recipientId);
-             }
-         } else {
-             // Retrieve profile to get the current identity key from the server
-             profileHelper.refreshRecipientProfile(recipientId);
-         }
-     }
-     @Override
-     public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
-         final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey);
-         return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText();
-     }
-     private byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
-         final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey);
-         return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized();
-     }
-     private Fingerprint computeSafetyNumberFingerprint(
-             final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey
-     ) {
-         return Utils.computeSafetyNumber(capabilities.isUuid(),
-                 account.getSelfAddress(),
-                 account.getIdentityKeyPair().getPublicKey(),
-                 theirAddress,
-                 theirIdentityKey);
+         this.identityHelper.handleIdentityFailure(recipientId, identityFailure);
      }
  
      @Override
          }
          account = null;
      }
  }
index 508a788113201f0cd196a6ae808191108579094f,550585805d35d6bd4025d101c99eee7d97828d0b..1a0fbfaaa76cd495b8d8628413b38b63aae80963
@@@ -29,7 -29,7 +29,7 @@@ method(arg1<type>, arg2<type>, ...) -> 
  
  Where <type> is according to DBus specification:
  
- * <a>   : Array of ... (comma-separated listarray:)
+ * <a>   : Array of ... (comma-separated list) (array:)
  * (...) : Struct (cannot be sent via `dbus-send`)
  * <b>   : Boolean (false|true) (boolean:)
  * <i>   : Signed 32-bit (int) integer (int32:)
@@@ -48,9 -48,9 +48,9 @@@ Phone numbers always have the format +<
  == Methods
  
  === Control methods
- These methods are available if the daemon is started anonymously (without an explicit `-u USERNAME`). 
- Requests are sent to `/org/asamk/Signal`; requests related to individual accounts are sent to 
- `/org/asamk/Signal/_441234567890` where the + dialing code is replaced by an underscore (_). 
+ These methods are available if the daemon is started anonymously (without an explicit `-u USERNAME`).
+ Requests are sent to `/org/asamk/Signal`; requests related to individual accounts are sent to
+ `/org/asamk/Signal/_441234567890` where the + dialing code is replaced by an underscore (_).
  Only `version()` is activated in single-user mode; the rest are disabled.
  
  link() -> deviceLinkUri<s>::
@@@ -58,12 -58,12 +58,12 @@@ link(newDeviceName<s>) -> deviceLinkUri
  * newDeviceName : Name to give new device (defaults to "cli" if no name is given)
  * deviceLinkUri : URI of newly linked device
  
- Returns a URI of the form "tsdevice:/?uuid=...". This can be piped to a QR encoder to create a display that
+ Returns a URI of the form "sgnl://linkdevice/?uuid=...". This can be piped to a QR encoder to create a display that
  can be captured by a Signal smartphone client. For example:
  
  `dbus-send --session --dest=org.asamk.Signal --type=method_call --print-reply /org/asamk/Signal org.asamk.Signal.link string:"My secondary client"|tr '\n' '\0'|sed 's/.*string //g'|sed 's/\"//g'|qrencode -s10 -tANSI256`
  
- Exception: Failure
+ Exceptions: Failure
  
  listAccounts() -> accountList<as>::
  * accountList : Array of all attached accounts in DBus object path form
@@@ -89,94 -89,243 +89,243 @@@ verify(number<s>, verificationCode<s>) 
  
  Command fails if PIN was set after previous registration; use verifyWithPin instead.
  
- Exception: Failure, InvalidNumber
+ Exceptions: Failure, InvalidNumber
  
  verifyWithPin(number<s>, verificationCode<s>, pin<s>) -> <>::
  * number            : Phone number
  * verificationCode  : Code received from Signal after successful registration request
  * pin               : PIN you set with setPin command after verifying previous registration
  
- Exception: Failure, InvalidNumber
+ Exceptions: Failure, InvalidNumber
  
  version() -> version<s>::
  * version : Version string of signal-cli
  
  Exceptions: None
  
- === Other methods
+ === Group control methods
+ The following methods listen to the recipient's object path, which is constructed as follows:
+ "/org/asamk/Signal/" + DBusNumber
+ * DBusNumber  : recipient's phone number, with underscore (_) replacing plus (+)
  
updateGroup(groupId<ay>, newName<s>, members<as>, avatar<s>) -> groupId<ay>::
- * groupId  : Byte array representing the internal group identifier
- * newName  : New name of group (empty if unchanged)
- * members  : String array of new members to be invited to group
- * avatar   : Filename of avatar picture to be set for group (empty if none)
createGroup(groupName<s>, members<as>, avatar<s>) -> groupId<ay>::
+ * groupName : String representing the display name of the group
+ * members   : String array of new members to be invited to group
+ * avatar    : Filename of avatar picture to be set for group (empty if none)
+ * groupId   : Byte array representing the internal group identifier
  
- Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound
+ Exceptions: AttachmentInvalid, Failure, InvalidNumber;
  
- updateProfile(name<s>, about<s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
- updateProfile(givenName<s>, familyName<s>, about<s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
- * name        : Name for your own profile (empty if unchanged)
- * givenName   : Given name for your own profile (empty if unchanged)
- * familyName  : Family name for your own profile (empty if unchanged)
- * about       : About message for profile (empty if unchanged)
- * aboutEmoji  : Emoji for profile (empty if unchanged)
- * avatar      : Filename of avatar picture for profile (empty if unchanged)
- * remove      : Set to true if the existing avatar picture should be removed
+ getGroup(groupId<ay>) -> objectPath<o>::
+ * groupId    : Byte array representing the internal group identifier
+ * objectPath : DBusPath for the group
+ getGroupMembers(groupId<ay>) -> members<as>::
+ * groupId   : Byte array representing the internal group identifier
+ * members   : String array with the phone numbers of all active members of a group
+ Exceptions: None, if the group name is not found an empty array is returned
+ joinGroup(inviteURI<s>) -> <>::
+ * inviteURI : String starting with https://signal.group/#
+ Behavior of this method depends on the `requirePermission` parameter of the `enableLink` method. If permission is required, `joinGroup` adds you to the requesting members list. Permission may be granted based on the group's `PermissionAddMember` property (`ONLY_ADMINS` or `EVERY_MEMBER`). If permission is not required, `joinGroup` admits you immediately to the group.
  
  Exceptions: Failure
  
+ listGroups() -> groups<a(oays)>::
+ * groups          : Array of Structs(objectPath, groupId, groupName)
+ ** objectPath      : DBusPath
+ ** groupId         : Byte array representing the internal group identifier
+ ** groupName       : String representing the display name of the group
  
- setExpirationTimer(number<s>, expiration<i>) -> <>::
- * number     : Phone number of recipient
- * expiration : int32 for the number of seconds before messages to this recipient disappear. Set to 0 to disable expiration.
+ sendGroupMessage(message<s>, attachments<as>, groupId<ay>) -> timestamp<x>::
+ * message     : Text to send (can be UTF8)
+ * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under)
+ * groupId     : Byte array representing the internal group identifier
+ * timestamp   : Long, can be used to identify the corresponding Signal reply
+ Exceptions: GroupNotFound, Failure, AttachmentInvalid, InvalidGroupId
+ sendGroupMessageReaction(emoji<s>, remove<b>, targetAuthor<s>, targetSentTimestamp<x>, groupId<ay>) -> timestamp<x>::
+ * emoji               : Unicode grapheme cluster of the emoji
+ * remove              : Boolean, whether a previously sent reaction (emoji) should be removed
+ * targetAuthor        : String with the phone number of the author of the message to which to react
+ * targetSentTimestamp : Long representing timestamp of the message to which to react
+ * groupId             : Byte array representing the internal group identifier
+ * timestamp           : Long, can be used to identify the corresponding signal reply
+ Exceptions: Failure, InvalidNumber, GroupNotFound, InvalidGroupId
+ sendGroupRemoteDeleteMessage(targetSentTimestamp<x>, groupId<ay>) -> timestamp<x>::
+ * targetSentTimestamp : Long representing timestamp of the message to delete
+ * groupId             : Byte array with base64 encoded group identifier
+ * timestamp           : Long, can be used to identify the corresponding signal reply
+ Exceptions: Failure, GroupNotFound, InvalidGroupId
+ === Group methods
+ The following methods listen to the group's object path, which can be obtained from the listGroups() method and is constructed as follows:
+ "/org/asamk/Signal/" + DBusNumber + "/Groups/" + DBusGroupId
+ * DBusNumber  : recipient's phone number, with underscore (_) replacing plus (+)
+ * DBusGroupId : groupId in base64 format, with underscore (_) replacing plus (+), equals (=), or slash (/)
+ Groups have the following (case-sensitive) properties:
+ * Id<ay> (read-only)                : Byte array representing the internal group identifier
+ * Name<s>                           : Display name of the group
+ * Description<s>                    : Description of the group
+ * Avatar<s> (write-only)            : Filename of the avatar
+ * IsBlocked<b>                      : true=member will not receive group messages; false=not blocked
+ * IsMember<b> (read-only)           : always true (object path exists only for group members)
+ * IsAdmin<b> (read-only)            : true=member has admin privileges; false=not admin
+ * MessageExpirationTimer<i>         : int32 representing message expiration time for group
+ * Members<as> (read-only)           : String array of group members' phone numbers
+ * PendingMembers<as> (read-only)    : String array of pending members' phone numbers
+ * RequestingMembers<as> (read-only) : String array of requesting members' phone numbers
+ * Admins<as> (read-only)            : String array of admins' phone numbers
+ * PermissionAddMember<s>            : String representing who has permission to add members
+ ** ONLY_ADMINS, EVERY_MEMBER
+ * PermissionEditDetails<s>          : String representing who may edit group details
+ ** ONLY_ADMINS, EVERY_MEMBER
+ * PermissionSendMessage<s>          : String representing who post messages to group
+ ** ONLY_ADMINS, EVERY_MEMBER (note that ONLY_ADMINS is equivalent to IsAnnouncementGroup)
+ * GroupInviteLink<s> (read-only)    : String of the invitation link (starts with https://signal.group/#)
+ To get a property, use (replacing `--session` with `--system` if needed):
+ `dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Get string:org.asamk.Signal.Group string:$PROPERTY_NAME`
+ To set a property, use:
+ `dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Set string:org.asamk.Signal.Group string:$PROPERTY_NAME variant:$PROPERTY_TYPE:$PROPERTY_VALUE`
+ To get all properties, use:
+ `dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.GetAll string:org.asamk.Signal.Group`
+ addAdmins(recipients<as>) -> <>::
+ * recipients  : String array of phone numbers
+ Grant admin privileges to recipients.
  
  Exceptions: Failure
  
- setContactBlocked(number<s>, block<b>) -> <>::
- * number  : Phone number affected by method
- * block   : 0=remove block , 1=blocked
+ addMembers(recipients<as>) -> <>::
+ * recipients  : String array of phone numbers
  
Messages from blocked numbers will no longer be forwarded via DBus.
Add recipients to group if they are pending members; otherwise add recipients to list of requesting members.
  
- Exceptions: InvalidNumber
+ Exceptions: Failure
  
- setGroupBlocked(groupId<ay>, block<b>) -> <>::
- * groupId : Byte array representing the internal group identifier
- * block   : 0=remove block , 1=blocked
+ disableLink() -> <>::
  
- Messages from blocked groups will no longer be forwarded via DBus.
+ Disables the group's invitation link.
+ Exceptions: Failure
  
- Exceptions: GroupNotFound
+ enableLink(requiresApproval<b>) -> <>::
+ * requiresApproval : true=add numbers using the link to the requesting members list
  
- joinGroup(inviteURI<s>) -> <>::
- * inviteURI : String starting with https://signal.group which is generated when you share a group link via Signal App
+ Enables the group's invitation link.
+ Exceptions: Failure
+ quitGroup() -> <>::
+ Exceptions: Failure, LastGroupAdmin
+ removeAdmins(recipients<as>) -> <>::
+ * recipients  : String array of phone numbers
+ Remove admin privileges from recipients.
  
  Exceptions: Failure
  
+ removeMembers(recipients<as>) -> <>::
+ * recipients  : String array of phone numbers
+ Remove recipients from group.
+ Exceptions: Failure
+ resetLink() -> <>::
+ Resets the group's invitation link to a new random URL starting with https://signal.group/#
+ Exceptions: Failure
+ === Deprecated group control methods
+ The following deprecated methods listen to the recipient's object path, which is constructed as follows:
+ "/org/asamk/Signal/" + DBusNumber
+ * DBusNumber  : recipient's phone number, with underscore (_) replacing plus (+)
+ getGroupIds() -> groupList<aay>::
+ groupList : Array of Byte arrays representing the internal group identifiers
+ All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked()
+ Exceptions: None
+ getGroupName(groupId<ay>) -> groupName<s>::
+ * groupId   : Byte array representing the internal group identifier
+ * groupName : The display name of the group
+ Exceptions: None, if the group name is not found an empty string is returned
+ isGroupBlocked(groupId<ay>) -> isGroupBlocked<b>::
+ * groupId        : Byte array representing the internal group identifier
+ * isGroupBlocked : true=group is blocked; false=group is not blocked
+ Dbus will not forward messages from a group when you have blocked it.
+ Exceptions: InvalidGroupId, Failure
+ isMember(groupId<ay>) -> isMember<b>::
+ * groupId   : Byte array representing the internal group identifier
+ * isMember  : true=you are a group member; false=you are not a group member
+ Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false)
  quitGroup(groupId<ay>) -> <>::
  * groupId : Byte array representing the internal group identifier
  
  Note that quitting a group will not remove the group from the getGroupIds command, but set it inactive which can be tested with isMember()
  
- Exceptions: GroupNotFound, Failure
+ Exceptions: GroupNotFound, Failure, InvalidGroupId
  
isMember(groupId<ay>) -> active<b>::
setGroupBlocked(groupId<ay>, block<b>) -> <>::
  * groupId : Byte array representing the internal group identifier
+ * block   : false=remove block , true=blocked
  
- Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false)
+ Messages from blocked groups will no longer be forwarded via DBus.
  
- sendEndSessionMessage(recipients<as>) -> <>::
- * recipients : Array of phone numbers 
+ Exceptions: GroupNotFound, InvalidGroupId
  
- Exceptions: Failure, InvalidNumber, UntrustedIdentity
+ updateGroup(groupId<ay>, newName<s>, members<as>, avatar<s>) -> groupId<ay>::
+ * groupId  : Byte array representing the internal group identifier
+ * newName  : New name of group (empty if unchanged)
+ * members  : String array of new members to be invited to group
+ * avatar   : Filename of avatar picture to be set for group (empty if none)
  
- sendGroupMessage(message<s>, attachments<as>, groupId<ay>) -> timestamp<x>::
- * message     : Text to send (can be UTF8)
- * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under)
- * groupId     : Byte array representing the internal group identifier
- * timestamp   : Can be used to identify the corresponding signal reply
+ Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound
+ === Device control methods
+ The following methods listen to the recipient's object path, which is constructed as follows:
+ "/org/asamk/Signal/" + DBusNumber
+ * DBusNumber  : recipient's phone number, with underscore (_) replacing plus (+)
+ addDevice(deviceUri<s>) -> <>::
+ * deviceUri : URI in the form of "sgnl://linkdevice/?uuid=..." (formerly "tsdevice:/?uuid=...") Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method.
+ getDevice(deviceId<x>) -> devicePath<o>::
+ * deviceId   : Long representing a deviceId
+ * devicePath : DBusPath object for the device
+ Exceptions: DeviceNotFound
+ listDevices() -> devices<a(oxs)>::
+ * devices      : Array of structs (objectPath, id, name)
+ ** objectPath   : DBusPath representing the device's object path
+ ** id           : Long representing the deviceId
+ ** name         : String representing the device's name
  
- Exceptions: GroupNotFound, Failure, AttachmentInvalid
+ Exceptions: InvalidUri
  
  sendContacts() -> <>::
  
@@@ -188,49 -337,103 +337,103 @@@ sendSyncRequest() -> <>:
  
  Sends a synchronization request to the primary device (for group, contacts, ...). Only works if sent from a secondary device.
  
- Exception: Failure
+ Exceptions: Failure
  
- sendNoteToSelfMessage(message<s>, attachments<as>) -> timestamp<x>::
- * message     : Text to send (can be UTF8)
- * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under)
- * timestamp   : Can be used to identify the corresponding signal reply
+ === Device methods and properties
+ The following methods listen to the device's object path, which is constructed as follows:
+ "/org/asamk/Signal/" + DBusNumber + "/Devices/" + deviceId
+ * DBusNumber  : recipient's phone number, with underscore (_) replacing plus (+)
+ * deviceId    : Long representing the device identifier (obtained from listDevices() method)
  
- Exceptions: Failure, AttachmentInvalid
+ Devices have the following (case-sensitive) properties:
+ * Id<x> (read-only)       : Long representing the device identifier
+ * Created<x> (read-only)  : Long representing the number of milliseconds since the Unix epoch
+ * LastSeen<x> (read-only) : Long representing the number of milliseconds since the Unix epoch
+ * Name<s>                 : String representing the display name of the device
  
- sendMessage(message<s>, attachments<as>, recipient<s>) -> timestamp<x>::
- sendMessage(message<s>, attachments<as>, recipients<as>) -> timestamp<x>::
- * message     : Text to send (can be UTF8)
- * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under)
- * recipient   : Phone number of a single recipient
- * recipients  : Array of phone numbers 
- * timestamp   : Can be used to identify the corresponding signal reply
+ To get a property, use (replacing `--session` with `--system` if needed):
+ `dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Get string:org.asamk.Signal.Device string:$PROPERTY_NAME`
  
- Depending on the type of the recipient field this sends a message to one or multiple recipients.
+ To set a property, use:
+ `dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Set string:org.asamk.Signal.Device string:$PROPERTY_NAME variant:$PROPERTY_TYPE:$PROPERTY_VALUE`
  
- Exceptions: AttachmentInvalid, Failure, InvalidNumber, UntrustedIdentity
+ To get all properties, use:
+ `dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.GetAll string:org.asamk.Signal.Device`
  
- sendTyping(recipient<s>, stop<b>) -> <>::
- * recipient             : Phone number of a single recipient
- * targetSentTimestamp   : True, if typing state should be stopped
+ removeDevice() -> <>::
  
- Exceptions: Failure, GroupNotFound, UntrustedIdentity
+ Exceptions: Failure
  
+ === Other methods
  
sendReadReceipt(recipient<s>, targetSentTimestamp<ax>) -> <>::
- * recipient             : Phone number of a single recipient
- * targetSentTimestamp   : Array of Longs to identify the corresponding signal messages
getContactName(number<s>) -> name<s>::
+ * number  : Phone number
+ * name    : Contact's name in local storage (from the master device for a linked account, or the one set with setContactName); if not set, contact's profile name is used
  
- Exceptions: Failure, UntrustedIdentity
+ Exceptions: None
  
- sendGroupMessageReaction(emoji<s>, remove<b>, targetAuthor<s>, targetSentTimestamp<x>, groupId<ay>) -> timestamp<x>::
- * emoji               : Unicode grapheme cluster of the emoji
- * remove              : Boolean, whether a previously sent reaction (emoji) should be removed
- * targetAuthor        : String with the phone number of the author of the message to which to react
- * targetSentTimestamp : Long representing timestamp of the message to which to react
- * groupId             : Byte array with base64 encoded group identifier
- * timestamp           : Long, can be used to identify the corresponding signal reply
+ getContactNumber(name<s>) -> numbers<as>::
+ * numbers : Array of phone number
+ * name    : Contact or profile name ("firstname lastname")
+ Searches contacts and known profiles for a given name and returns the list of all known numbers. May result in e.g. two entries if a contact and profile name is set.
+ Exceptions: None
+ getSelfNumber() -> number<s>::
+ * number : Your phone number
+ Exceptions: None
+ isContactBlocked(number<s>) -> blocked<b>::
+ * number    : Phone number
+ * blocked   : true=blocked, false=not blocked
+ For unknown numbers false is returned but no exception is raised.
+ Exceptions: InvalidPhoneNumber
+ isRegistered() -> result<b>::
+ isRegistered(number<s>) -> result<b>::
+ isRegistered(numbers<as>) -> results<ab>::
+ * number  : Phone number
+ * numbers : String array of phone numbers
+ * result  : true=number is registered, false=number is not registered
+ * results : Boolean array of results
+ For unknown numbers, false is returned, but no exception is raised. If no number is given, returns true (indicating that you are registered).
+ Exceptions: InvalidNumber
+ listNumbers() -> numbers<as>::
+ * numbers : String array of all known numbers
+ This is a concatenated list of all defined contacts as well of profiles known (e.g. peer group members or sender of received messages)
+ Exceptions: None
+ removePin() -> <>::
+ Removes registration PIN protection.
+ Exceptions: Failure
+ sendEndSessionMessage(recipients<as>) -> <>::
+ * recipients : Array of phone numbers
+ Exceptions: Failure, InvalidNumber, UntrustedIdentity
+ sendMessage(message<s>, attachments<as>, recipient<s>) -> timestamp<x>::
+ sendMessage(message<s>, attachments<as>, recipients<as>) -> timestamp<x>::
+ * message     : Text to send (can be UTF8)
+ * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under)
+ * recipient   : Phone number of a single recipient
+ * recipients  : String array of phone numbers
+ * timestamp   : Long, can be used to identify the corresponding Signal reply
  
- Exceptions: Failure, InvalidNumber, GroupNotFound
+ Depending on the type of the recipient field this sends a message to one or multiple recipients.
+ Exceptions: AttachmentInvalid, Failure, InvalidNumber, UntrustedIdentity
  
  sendMessageReaction(emoji<s>, remove<b>, targetAuthor<s>, targetSentTimestamp<x>, recipient<s>) -> timestamp<x>::
  sendMessageReaction(emoji<s>, remove<b>, targetAuthor<s>, targetSentTimestamp<x>, recipients<as>) -> timestamp<x>::
  * targetSentTimestamp : Long representing timestamp of the message to which to react
  * recipient           : String with the phone number of a single recipient
  * recipients          : Array of strings with phone numbers, should there be more recipients
- * timestamp           : Long, can be used to identify the corresponding signal reply
+ * timestamp           : Long, can be used to identify the corresponding Signal reply
  
  Depending on the type of the recipient(s) field this sends a reaction to one or multiple recipients.
  
  Exceptions: Failure, InvalidNumber
  
- sendGroupRemoteDeleteMessage(targetSentTimestamp<x>, groupId<ay>) -> timestamp<x>::
- * targetSentTimestamp : Long representing timestamp of the message to delete
- * groupId             : Byte array with base64 encoded group identifier
- * timestamp           : Long, can be used to identify the corresponding signal reply
+ sendNoteToSelfMessage(message<s>, attachments<as>) -> timestamp<x>::
+ * message     : Text to send (can be UTF8)
+ * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under)
+ * timestamp   : Long, can be used to identify the corresponding Signal reply
  
- Exceptions: Failure, GroupNotFound
+ Exceptions: Failure, AttachmentInvalid
+ sendReadReceipt(recipient<s>, targetSentTimestamps<ax>) -> <>::
+ * recipient             : Phone number of a single recipient
+ * targetSentTimestamps  : Array of Longs to identify the corresponding Signal messages
+ Exceptions: Failure, UntrustedIdentity
  
  sendRemoteDeleteMessage(targetSentTimestamp<x>, recipient<s>) -> timestamp<x>::
  sendRemoteDeleteMessage(targetSentTimestamp<x>, recipients<as>) -> timestamp<x>::
@@@ -264,140 -473,78 +473,98 @@@ Depending on the type of the recipient(
  
  Exceptions: Failure, InvalidNumber
  
- getContactName(number<s>) -> name<s>::
- * number  : Phone number
- * name    : Contact's name in local storage (from the primary device for a linked account, or the one set with setContactName); if not set, contact's profile name is used
- setContactName(number<s>,name<>) -> <>::
- * number  : Phone number
- * name    : Name to be set in contacts (in local storage with signal-cli)
- getGroupIds() -> groupList<aay>::
- groupList : Array of Byte arrays representing the internal group identifiers
- All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked()
- getGroupName(groupId<ay>) -> groupName<s>::
- groupName : The display name of the group 
- groupId   : Byte array representing the internal group identifier
- Exceptions: None, if the group name is not found an empty string is returned
- getGroupMembers(groupId<ay>) -> members<as>::
- members   : String array with the phone numbers of all active members of a group
- groupId   : Byte array representing the internal group identifier
- Exceptions: None, if the group name is not found an empty array is returned
+ sendTyping(recipient<s>, stop<b>) -> <>::
+ * recipient             : Phone number of a single recipient
+ * targetSentTimestamp   : True, if typing state should be stopped
  
- listNumbers() -> numbers<as>::
- numbers : String array of all known numbers
+ Exceptions: Failure, GroupNotFound, UntrustedIdentity
  
- This is a concatenated list of all defined contacts as well of profiles known (e.g. peer group members or sender of received messages)
+ setContactBlocked(number<s>, block<b>) -> <>::
+ * number  : Phone number affected by method
+ * block   : false=remove block, true=blocked
  
- getContactNumber(name<s>) -> numbers<as>::
- * numbers : Array of phone number
- * name    : Contact or profile name ("firstname lastname")
+ Messages from blocked numbers will no longer be forwarded via DBus.
  
- Searches contacts and known profiles for a given name and returns the list of all known numbers. May result in e.g. two entries if a contact and profile name is set.
+ Exceptions: InvalidNumber
  
isContactBlocked(number<s>) -> state<b>::
setContactName(number<s>,name<>) -> <>::
  * number  : Phone number
- * state   : 1=blocked, 0=not blocked
- Exceptions: None, for unknown numbers 0 (false) is returned
- isGroupBlocked(groupId<ay>) -> state<b>::
- * groupId : Byte array representing the internal group identifier
- * state   : 1=blocked, 0=not blocked
- Exceptions: None, for unknown groups 0 (false) is returned
+ * name    : Name to be set in contacts (in local storage with signal-cli)
  
- removePin() -> <>::
+ Exceptions: InvalidNumber, Failure
  
- Removes registration PIN protection.
+ setExpirationTimer(number<s>, expiration<i>) -> <>::
+ * number     : Phone number of recipient
+ * expiration : int32 for the number of seconds before messages to this recipient disappear. Set to 0 to disable expiration.
  
- Exception: Failure
+ Exceptions: Failure, InvalidNumber
  
  setPin(pin<s>) -> <>::
  * pin               : PIN you set after registration (resets after 7 days of inactivity)
  
  Sets a registration lock PIN, to prevent others from registering your number.
  
- Exception: Failure
- version() -> version<s>::
- * version : Version string of signal-cli
- isRegistered() -> result<b>::
- isRegistered(number<s>) -> result<b>::
- isRegistered(numbers<as>) -> results<ab>::
- * number  : Phone number
- * numbers : String array of phone numbers
- * result  : true=number is registered, false=number is not registered
- * results : Boolean array of results
- Exception: InvalidNumber for an incorrectly formatted phone number. For unknown numbers, false is returned, but no exception is raised. If no number is given, returns whether you are registered (presumably true).
- addDevice(deviceUri<s>) -> <>::
- * deviceUri : URI in the form of tsdevice:/?uuid=... Normally received from Signal desktop or smartphone app
- Exception: InvalidUri
- listDevices() -> devices<a(oxs)>::
- * devices      : Array of structs (objectPath, id, name)
- ** objectPath   : DBusPath representing the device's object path
- ** id           : Long representing the deviceId
- ** name         : String representing the device's name
- Exception: Failure
- removeDevice(deviceId<i>) -> <>::
- * deviceId : Device ID to remove, obtained from listDevices() command
+ Exceptions: Failure
  
- Exception: Failure
+ submitRateLimitChallenge(challenge<s>, captcha<s>) -> <>::
+ * challenge : The challenge token taken from the proof required error.
+ * captcha   : The captcha token from the solved captcha on the Signal website..
+ Can be used to lift some rate-limits by solving a captcha.
  
- updateDeviceName(deviceName<s>) -> <>::
- * deviceName : New name 
+ Exception: IOErrorException
  
- Set a new name for this device (main or linked).
+ updateProfile(name<s>, about<s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
+ updateProfile(givenName<s>, familyName<s>, about<s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
+ * name        : Name for your own profile (empty if unchanged)
+ * givenName   : Given name for your own profile (empty if unchanged)
+ * familyName  : Family name for your own profile (empty if unchanged)
+ * about       : About message for profile (empty if unchanged)
+ * aboutEmoji  : Emoji for profile (empty if unchanged)
+ * avatar      : Filename of avatar picture for profile (empty if unchanged)
+ * remove      : Set to true if the existing avatar picture should be removed
  
- Exception: Failure
+ Exceptions: Failure
  
  uploadStickerPack(stickerPackPath<s>) -> url<s>::
  * stickerPackPath : Path to the manifest.json file or a zip file in the same directory
  * url             : URL of sticker pack after successful upload
  
 -Exceptions: Failure
 +Exception: Failure, IOError
 +
 +getConfiguration() -> [readReceipts<b>, unidentifiedDeliveryIndicators<b>, typingIndicators<b>, linkPreviews<b>] -> <>::
 +* readReceipts                   : Should Signal send read receipts (true/false).
 +* unidentifiedDeliveryIndicators : Should Signal show unidentified delivery indicators (true/false).
 +* typingIndicators               : Should Signal send/show typing indicators (true/false).
 +* linkPreviews                   : Should Signal generate link previews (true/false).
 +
 +Gets an array of four booleans as indicated. Only works from primary device.
 +
 +Exceptions: IOError, UserError
 +
 +setConfiguration(readReceipts<b>, unidentifiedDeliveryIndicators<b>, typingIndicators<b>, linkPreviews<b>) -> <>::
 +* readReceipts                   : Should Signal send read receipts (true/false).
 +* unidentifiedDeliveryIndicators : Should Signal show unidentified delivery indicators (true/false).
 +* typingIndicators               : Should Signal send/show typing indicators (true/false).
 +* linkPreviews                   : Should Signal generate link previews (true/false).
 +
 +Update Signal configurations and sync them to linked devices. Only works from primary device.
 +
 +Exceptions: IOError, UserError
  
- submitRateLimitChallenge(challenge<s>, captcha<s>) -> <>::
- * challenge : The challenge token taken from the proof required error.
- * captcha   : The captcha token from the solved captcha on the Signal website..
- Can be used to lift some rate-limits by solving a captcha.
+ version() -> version<s>::
+ * version : Version string of signal-cli
  
- Exception: IOErrorException
+ Exceptions: None
  
  == Signals
- SyncMessageReceived (timestamp<x>, sender<s>, destination<s>, groupId<ay>,message<s>, attachments<as>)::
+ SyncMessageReceived (timestamp<x>, sender<s>, destination<s>, groupId<ay>, message<s>, attachments<as>)::
+ * timestamp   : Integer value that can be used to associate this e.g. with a sendMessage()
+ * sender      : Phone number of the sender
+ * destination : DBus code for destination
+ * groupId     : Byte array representing the internal group identifier (empty when private message)
+ * message     : Message text
+ * attachments : String array of filenames in the signal-cli storage (~/.local/share/signal-cli/attachments/)
  The sync message is received when the user sends a message from a linked device.
  
  ReceiptReceived (timestamp<x>, sender<s>)::
@@@ -411,7 -558,7 +578,7 @@@ MessageReceived(timestamp<x>, sender<s>
  * sender      : Phone number of the sender
  * groupId     : Byte array representing the internal group identifier (empty when private message)
  * message     : Message text
- * attachments : String array of filenames for the attachments. These files are located in the signal-cli storage and the current user needs to have read access there
+ * attachments : String array of filenames in the signal-cli storage (~/.local/share/signal-cli/attachments/)
  
  This signal is received whenever we get a private message or a message is posted in a group we are an active member
  
index 865db815c14a26745c4a2ff237028b4184c11eeb,349671b37bffb05e9fa058862046adb8b2afd6af..28c192a09557281bac20361cd4b9058c14925fe3
@@@ -1,7 -1,6 +1,6 @@@
  package org.asamk;
  
  import org.asamk.signal.commands.exceptions.IOErrorException;
  import org.freedesktop.dbus.DBusPath;
  import org.freedesktop.dbus.Struct;
  import org.freedesktop.dbus.annotations.DBusProperty;
@@@ -84,14 -83,27 +83,27 @@@ public interface Signal extends DBusInt
  
      void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber;
  
+     @Deprecated
      void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound, Error.InvalidGroupId;
  
+     @Deprecated
      List<byte[]> getGroupIds();
  
+     DBusPath getGroup(byte[] groupId);
+     List<StructGroup> listGroups();
+     @Deprecated
      String getGroupName(byte[] groupId) throws Error.InvalidGroupId;
  
+     @Deprecated
      List<String> getGroupMembers(byte[] groupId) throws Error.InvalidGroupId;
  
+     byte[] createGroup(
+             String name, List<String> members, String avatar
+     ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber;
+     @Deprecated
      byte[] updateGroup(
              byte[] groupId, String name, List<String> members, String avatar
      ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.InvalidGroupId;
  
      List<String> getContactNumber(final String name) throws Error.Failure;
  
+     @Deprecated
      void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure, Error.InvalidGroupId;
  
      boolean isContactBlocked(final String number) throws Error.InvalidNumber;
  
+     @Deprecated
      boolean isGroupBlocked(final byte[] groupId) throws Error.InvalidGroupId;
  
+     @Deprecated
      boolean isMember(final byte[] groupId) throws Error.InvalidGroupId;
  
      byte[] joinGroup(final String groupLink) throws Error.Failure;
  
      String uploadStickerPack(String stickerPackPath) throws Error.Failure;
  
 +    void setConfiguration(boolean readReceipts, boolean unidentifiedDeliveryIndicators, boolean typingIndicators, boolean linkPreviews) throws Error.IOError, Error.UserError;
 +
 +    List<Boolean> getConfiguration();
 +
      void submitRateLimitChallenge(String challenge, String captchaString) throws IOErrorException;
  
      class MessageReceived extends DBusSignal {
          void removeDevice() throws Error.Failure;
      }
  
+     class StructGroup extends Struct {
+         @Position(0)
+         DBusPath objectPath;
+         @Position(1)
+         byte[] id;
+         @Position(2)
+         String name;
+         public StructGroup(final DBusPath objectPath, final byte[] id, final String name) {
+             this.objectPath = objectPath;
+             this.id = id;
+             this.name = name;
+         }
+         public DBusPath getObjectPath() {
+             return objectPath;
+         }
+         public byte[] getId() {
+             return id;
+         }
+         public String getName() {
+             return name;
+         }
+     }
+     @DBusProperty(name = "Id", type = Byte[].class, access = DBusProperty.Access.READ)
+     @DBusProperty(name = "Name", type = String.class)
+     @DBusProperty(name = "Description", type = String.class)
+     @DBusProperty(name = "Avatar", type = String.class, access = DBusProperty.Access.WRITE)
+     @DBusProperty(name = "IsBlocked", type = Boolean.class)
+     @DBusProperty(name = "IsMember", type = Boolean.class, access = DBusProperty.Access.READ)
+     @DBusProperty(name = "IsAdmin", type = Boolean.class, access = DBusProperty.Access.READ)
+     @DBusProperty(name = "MessageExpirationTimer", type = Integer.class)
+     @DBusProperty(name = "Members", type = String[].class, access = DBusProperty.Access.READ)
+     @DBusProperty(name = "PendingMembers", type = String[].class, access = DBusProperty.Access.READ)
+     @DBusProperty(name = "RequestingMembers", type = String[].class, access = DBusProperty.Access.READ)
+     @DBusProperty(name = "Admins", type = String[].class, access = DBusProperty.Access.READ)
+     @DBusProperty(name = "PermissionAddMember", type = String.class)
+     @DBusProperty(name = "PermissionEditDetails", type = String.class)
+     @DBusProperty(name = "PermissionSendMessage", type = String.class)
+     @DBusProperty(name = "GroupInviteLink", type = String.class, access = DBusProperty.Access.READ)
+     interface Group extends DBusInterface, Properties {
+         void quitGroup() throws Error.Failure, Error.LastGroupAdmin;
+         void addMembers(List<String> recipients) throws Error.Failure;
+         void removeMembers(List<String> recipients) throws Error.Failure;
+         void addAdmins(List<String> recipients) throws Error.Failure;
+         void removeAdmins(List<String> recipients) throws Error.Failure;
+         void resetLink() throws Error.Failure;
+         void disableLink() throws Error.Failure;
+         void enableLink(boolean requiresApproval) throws Error.Failure;
+     }
      interface Error {
  
          class AttachmentInvalid extends DBusExecutionException {
              }
          }
  
+         class DeviceNotFound extends DBusExecutionException {
+             public DeviceNotFound(final String message) {
+                 super(message);
+             }
+         }
          class GroupNotFound extends DBusExecutionException {
  
              public GroupNotFound(final String message) {
              }
          }
  
+         class LastGroupAdmin extends DBusExecutionException {
+             public LastGroupAdmin(final String message) {
+                 super(message);
+             }
+         }
          class InvalidNumber extends DBusExecutionException {
  
              public InvalidNumber(final String message) {
                  super(message);
              }
          }
 +
 +        class IOError extends DBusExecutionException {
 +
 +            public IOError(final String message) {
 +                super(message);
 +            }
 +        }
 +
 +        class UserError extends DBusExecutionException {
 +
 +            public UserError(final String message) {
 +                super(message);
 +            }
 +        }
      }
  }
index 226402ddaa277fe8eebda1c7af6a4d1fbd00b7c5,59422e6922354ad01b454e2976dcbbb00ea6f11c..a8f071f7a6723c4510944a12b4490c1ab305cc62
@@@ -15,9 -15,9 +15,9 @@@ import org.asamk.signal.manager.api.Rec
  import org.asamk.signal.manager.api.SendGroupMessageResults;
  import org.asamk.signal.manager.api.SendMessageResults;
  import org.asamk.signal.manager.api.TypingAction;
+ import org.asamk.signal.manager.api.UpdateGroup;
  import org.asamk.signal.manager.groups.GroupId;
  import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
- import org.asamk.signal.manager.groups.GroupLinkState;
  import org.asamk.signal.manager.groups.GroupNotFoundException;
  import org.asamk.signal.manager.groups.GroupPermission;
  import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
@@@ -30,7 -30,6 +30,6 @@@ import org.freedesktop.dbus.DBusPath
  import org.freedesktop.dbus.connections.impl.DBusConnection;
  import org.freedesktop.dbus.exceptions.DBusException;
  import org.freedesktop.dbus.interfaces.DBusInterface;
- import org.whispersystems.libsignal.IdentityKey;
  import org.whispersystems.libsignal.InvalidKeyException;
  import org.whispersystems.libsignal.util.Pair;
  import org.whispersystems.libsignal.util.guava.Optional;
@@@ -108,17 -107,7 +107,17 @@@ public class DbusManagerImpl implement
              final Boolean typingIndicators,
              final Boolean linkPreviews
      ) throws IOException {
 -        throw new UnsupportedOperationException();
 +        signal.setConfiguration(
 +                readReceipts,
 +                unidentifiedDeliveryIndicators,
 +                typingIndicators,
 +                linkPreviews
 +                );
 +    }
 +
 +    @Override
 +    public List<Boolean> getConfiguration() {
 +        return signal.getConfiguration();
      }
  
      @Override
  
      @Override
      public List<Group> getGroups() {
-         final var groupIds = signal.getGroupIds();
-         return groupIds.stream().map(id -> getGroup(GroupId.unknownVersion(id))).collect(Collectors.toList());
+         final var groups = signal.listGroups();
+         return groups.stream().map(Signal.StructGroup::getObjectPath).map(this::getGroup).collect(Collectors.toList());
      }
  
      @Override
          if (groupAdmins.size() > 0) {
              throw new UnsupportedOperationException();
          }
-         signal.quitGroup(groupId.serialize());
+         final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
+         group.quitGroup();
          return new SendGroupMessageResults(0, List.of());
      }
  
      public Pair<GroupId, SendGroupMessageResults> createGroup(
              final String name, final Set<RecipientIdentifier.Single> members, final File avatarFile
      ) throws IOException, AttachmentInvalidException {
-         final var newGroupId = signal.updateGroup(new byte[0],
-                 emptyIfNull(name),
+         final var newGroupId = signal.createGroup(emptyIfNull(name),
                  members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()),
                  avatarFile == null ? "" : avatarFile.getPath());
          return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
  
      @Override
      public SendGroupMessageResults updateGroup(
-             final GroupId groupId,
-             final String name,
-             final String description,
-             final Set<RecipientIdentifier.Single> members,
-             final Set<RecipientIdentifier.Single> removeMembers,
-             final Set<RecipientIdentifier.Single> admins,
-             final Set<RecipientIdentifier.Single> removeAdmins,
-             final boolean resetGroupLink,
-             final GroupLinkState groupLinkState,
-             final GroupPermission addMemberPermission,
-             final GroupPermission editDetailsPermission,
-             final File avatarFile,
-             final Integer expirationTimer,
-             final Boolean isAnnouncementGroup
+             final GroupId groupId, final UpdateGroup updateGroup
      ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
-         signal.updateGroup(groupId.serialize(),
-                 emptyIfNull(name),
-                 members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()),
-                 avatarFile == null ? "" : avatarFile.getPath());
+         final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
+         if (updateGroup.getName() != null) {
+             group.Set("org.asamk.Signal.Group", "Name", updateGroup.getName());
+         }
+         if (updateGroup.getDescription() != null) {
+             group.Set("org.asamk.Signal.Group", "Description", updateGroup.getDescription());
+         }
+         if (updateGroup.getAvatarFile() != null) {
+             group.Set("org.asamk.Signal.Group",
+                     "Avatar",
+                     updateGroup.getAvatarFile() == null ? "" : updateGroup.getAvatarFile().getPath());
+         }
+         if (updateGroup.getExpirationTimer() != null) {
+             group.Set("org.asamk.Signal.Group", "MessageExpirationTimer", updateGroup.getExpirationTimer());
+         }
+         if (updateGroup.getAddMemberPermission() != null) {
+             group.Set("org.asamk.Signal.Group", "PermissionAddMember", updateGroup.getAddMemberPermission().name());
+         }
+         if (updateGroup.getEditDetailsPermission() != null) {
+             group.Set("org.asamk.Signal.Group", "PermissionEditDetails", updateGroup.getEditDetailsPermission().name());
+         }
+         if (updateGroup.getIsAnnouncementGroup() != null) {
+             group.Set("org.asamk.Signal.Group",
+                     "PermissionSendMessage",
+                     updateGroup.getIsAnnouncementGroup()
+                             ? GroupPermission.ONLY_ADMINS.name()
+                             : GroupPermission.EVERY_MEMBER.name());
+         }
+         if (updateGroup.getMembers() != null) {
+             group.addMembers(updateGroup.getMembers()
+                     .stream()
+                     .map(RecipientIdentifier.Single::getIdentifier)
+                     .collect(Collectors.toList()));
+         }
+         if (updateGroup.getRemoveMembers() != null) {
+             group.removeMembers(updateGroup.getRemoveMembers()
+                     .stream()
+                     .map(RecipientIdentifier.Single::getIdentifier)
+                     .collect(Collectors.toList()));
+         }
+         if (updateGroup.getAdmins() != null) {
+             group.addAdmins(updateGroup.getAdmins()
+                     .stream()
+                     .map(RecipientIdentifier.Single::getIdentifier)
+                     .collect(Collectors.toList()));
+         }
+         if (updateGroup.getRemoveAdmins() != null) {
+             group.removeAdmins(updateGroup.getRemoveAdmins()
+                     .stream()
+                     .map(RecipientIdentifier.Single::getIdentifier)
+                     .collect(Collectors.toList()));
+         }
+         if (updateGroup.isResetGroupLink()) {
+             group.resetLink();
+         }
+         if (updateGroup.getGroupLinkState() != null) {
+             switch (updateGroup.getGroupLinkState()) {
+                 case DISABLED:
+                     group.disableLink();
+                     break;
+                 case ENABLED:
+                     group.enableLink(false);
+                     break;
+                 case ENABLED_WITH_APPROVAL:
+                     group.enableLink(true);
+                     break;
+             }
+         }
          return new SendGroupMessageResults(0, List.of());
      }
  
      public void setGroupBlocked(
              final GroupId groupId, final boolean blocked
      ) throws GroupNotFoundException, IOException {
-         signal.setGroupBlocked(groupId.serialize(), blocked);
+         setGroupProperty(groupId, "IsBlocked", blocked);
+     }
+     private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) {
+         final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
+         group.Set("org.asamk.Signal.Group", propertyName, blocked);
      }
  
      @Override
  
      @Override
      public Group getGroup(final GroupId groupId) {
-         final var id = groupId.serialize();
-         return new Group(groupId,
-                 signal.getGroupName(id),
-                 null,
-                 null,
-                 signal.getGroupMembers(id).stream().map(m -> new RecipientAddress(null, m)).collect(Collectors.toSet()),
-                 Set.of(),
-                 Set.of(),
-                 Set.of(),
-                 signal.isGroupBlocked(id),
-                 0,
-                 false,
-                 signal.isMember(id));
+         final var groupPath = signal.getGroup(groupId.serialize());
+         return getGroup(groupPath);
+     }
+     @SuppressWarnings("unchecked")
+     private Group getGroup(final DBusPath groupPath) {
+         final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
+         final var id = (byte[]) group.get("Id").getValue();
+         try {
+             return new Group(GroupId.unknownVersion(id),
+                     (String) group.get("Name").getValue(),
+                     (String) group.get("Description").getValue(),
+                     GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
+                     ((List<String>) group.get("Members").getValue()).stream()
+                             .map(m -> new RecipientAddress(null, m))
+                             .collect(Collectors.toSet()),
+                     ((List<String>) group.get("PendingMembers").getValue()).stream()
+                             .map(m -> new RecipientAddress(null, m))
+                             .collect(Collectors.toSet()),
+                     ((List<String>) group.get("RequestingMembers").getValue()).stream()
+                             .map(m -> new RecipientAddress(null, m))
+                             .collect(Collectors.toSet()),
+                     ((List<String>) group.get("Admins").getValue()).stream()
+                             .map(m -> new RecipientAddress(null, m))
+                             .collect(Collectors.toSet()),
+                     (boolean) group.get("IsBlocked").getValue(),
+                     (int) group.get("MessageExpirationTimer").getValue(),
+                     GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
+                     GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
+                     GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
+                     (boolean) group.get("IsMember").getValue(),
+                     (boolean) group.get("IsAdmin").getValue());
+         } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
+             throw new AssertionError(e);
+         }
      }
  
      @Override
          throw new UnsupportedOperationException();
      }
  
-     @Override
-     public String computeSafetyNumber(
-             final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey
-     ) {
-         throw new UnsupportedOperationException();
-     }
      @Override
      public SignalServiceAddress resolveSignalServiceAddress(final SignalServiceAddress address) {
          return address;
index 41eca5da388306c979e8011dd8876fb0a9c0f38b,2cf3c813ff8adaa6965004998667001f1abd6fd9..c6e3273ae4bfdad834908ede0837dd6df4883498
@@@ -1,7 -1,6 +1,6 @@@
  package org.asamk.signal.dbus;
  
  import org.asamk.Signal;
- import org.asamk.Signal.Error;
  import org.asamk.signal.BaseConfig;
  import org.asamk.signal.commands.exceptions.IOErrorException;
  import org.asamk.signal.manager.AttachmentInvalidException;
@@@ -13,9 -12,12 +12,12 @@@ import org.asamk.signal.manager.api.Ide
  import org.asamk.signal.manager.api.Message;
  import org.asamk.signal.manager.api.RecipientIdentifier;
  import org.asamk.signal.manager.api.TypingAction;
+ import org.asamk.signal.manager.api.UpdateGroup;
  import org.asamk.signal.manager.groups.GroupId;
  import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+ import org.asamk.signal.manager.groups.GroupLinkState;
  import org.asamk.signal.manager.groups.GroupNotFoundException;
+ import org.asamk.signal.manager.groups.GroupPermission;
  import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
  import org.asamk.signal.manager.groups.LastGroupAdminException;
  import org.asamk.signal.manager.groups.NotAGroupMemberException;
@@@ -26,6 -28,7 +28,7 @@@ 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.freedesktop.dbus.types.Variant;
  import org.whispersystems.libsignal.InvalidKeyException;
  import org.whispersystems.libsignal.util.Pair;
  import org.whispersystems.libsignal.util.guava.Optional;
@@@ -40,6 -43,8 +43,8 @@@ 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;
@@@ -58,6 -63,7 +63,7 @@@ public class DbusSignalImpl implements 
  
      private DBusPath thisDevice;
      private final List<StructDevice> devices = new ArrayList<>();
+     private final List<StructGroup> groups = new ArrayList<>();
  
      public DbusSignalImpl(final Manager m, DBusConnection connection, final String objectPath) {
          this.m = m;
@@@ -67,6 -73,7 +73,7 @@@
  
      public void initObjects() {
          updateDevices();
+         updateGroups();
      }
  
      public void close() {
      @Override
      public DBusPath getDevice(long deviceId) {
          updateDevices();
-         return new DBusPath(getDeviceObjectPath(objectPath, deviceId));
+         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 void setGroupBlocked(final byte[] groupId, final boolean blocked) {
          try {
              m.setGroupBlocked(getGroupId(groupId), blocked);
+         } catch (NotMasterDeviceException 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) {
          return ids;
      }
  
+     @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 groupOptional.get().getObjectPath();
+     }
+     @Override
+     public List<StructGroup> listGroups() {
+         updateGroups();
+         return groups;
+     }
      @Override
      public String getGroupName(final byte[] groupId) {
          var group = m.getGroup(getGroupId(groupId));
          if (group == null) {
              return List.of();
          } else {
-             return group.getMembers().stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList());
+             final var members = group.getMembers();
+             return getRecipientStrings(members);
          }
      }
  
+     @Override
+     public byte[] createGroup(
+             final String name, final List<String> members, final String avatar
+     ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber {
+         return updateGroup(new byte[0], name, members, avatar);
+     }
      @Override
      public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) {
          try {
                  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 == null ? null : new File(avatar))
+                                 .build());
                  if (results != null) {
                      checkSendMessageResults(results.getTimestamp(), results.getResults());
                  }
          try {
              return m.uploadStickerPack(path).toString();
          } catch (IOException e) {
 -            throw new Error.Failure("Upload error (maybe image size is too large):" + e.getMessage());
 +            throw new Error.IOError("Upload error (maybe image size is too large):" + e.getMessage());
          } catch (StickerPackInvalidException e) {
              throw new Error.Failure("Invalid sticker pack: " + e.getMessage());
          }
      }
  
 +    @Override
 +    public void setConfiguration(boolean readReceipts, boolean unidentifiedDeliveryIndicators, boolean typingIndicators, boolean linkPreviews) {
 +          try {
 +              m.updateConfiguration(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews);
 +          } catch (IOException e) {
 +              throw new Error.IOError("UpdateAccount error: " + e.getMessage());
 +          } catch (NotMasterDeviceException e) {
 +              throw new Error.UserError("This command doesn't work on linked devices.");
 +          }
 +    }
 +
 +    @Override
 +    public List<Boolean> getConfiguration() {
 +        List<Boolean> config = new ArrayList<>(4);
 +        try {
 +            config = m.getConfiguration();
 +        } catch (IOException e) {
 +            throw new Error.IOError("Configuration storage error: " + e.getMessage());
 +        } catch (NotMasterDeviceException e) {
 +            throw new Error.UserError("This command doesn't work on linked devices.");
 +        }
 +        return config;
 +    }
 +
      private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException {
          var error = ErrorUtils.getErrorMessageFromSendMessageResult(result);
  
          throw new Error.Failure(message.toString());
      }
  
+     private static List<String> getRecipientStrings(final Set<RecipientAddress> members) {
+         return members.stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList());
+     }
      private static Set<RecipientIdentifier.Single> getSingleRecipientIdentifiers(
              final Collection<String> recipientStrings, final String localNumber
      ) throws DBusExecutionException {
          this.devices.clear();
      }
  
+     private static String getGroupObjectPath(String basePath, byte[] groupId) {
+         return basePath + "/Groups/" + Base64.getEncoder()
+                 .encodeToString(groupId)
+                 .replace("+", "_")
+                 .replace("/", "_")
+                 .replace("=", "_");
+     }
+     private void updateGroups() {
+         List<org.asamk.signal.manager.api.Group> groups;
+         groups = m.getGroups();
+         unExportGroups();
+         groups.forEach(g -> {
+             final var object = new DbusSignalGroupImpl(g.getGroupId());
+             try {
+                 connection.exportObject(object);
+             } catch (DBusException e) {
+                 e.printStackTrace();
+             }
+             this.groups.add(new StructGroup(new DBusPath(object.getObjectPath()),
+                     g.getGroupId().serialize(),
+                     emptyIfNull(g.getTitle())));
+         });
+     }
+     private void unExportGroups() {
+         this.groups.stream().map(StructGroup::getObjectPath).map(DBusPath::getPath).forEach(connection::unExportObject);
+         this.groups.clear();
+     }
      public class DbusSignalDeviceImpl extends DbusProperties implements Signal.Device {
  
          private final org.asamk.signal.manager.api.Device device;
              }
          }
      }
+     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().getTitle()), this::setGroupName),
+                             new DbusProperty<>("Description",
+                                     () -> emptyIfNull(getGroup().getDescription()),
+                                     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().getMessageExpirationTimer(),
+                                     this::setMessageExpirationTime),
+                             new DbusProperty<>("Members",
+                                     () -> new Variant<>(getRecipientStrings(getGroup().getMembers()), "as")),
+                             new DbusProperty<>("PendingMembers",
+                                     () -> new Variant<>(getRecipientStrings(getGroup().getPendingMembers()), "as")),
+                             new DbusProperty<>("RequestingMembers",
+                                     () -> new Variant<>(getRecipientStrings(getGroup().getRequestingMembers()), "as")),
+                             new DbusProperty<>("Admins",
+                                     () -> new Variant<>(getRecipientStrings(getGroup().getAdminMembers()), "as")),
+                             new DbusProperty<>("PermissionAddMember",
+                                     () -> getGroup().getPermissionAddMember().name(),
+                                     this::setGroupPermissionAddMember),
+                             new DbusProperty<>("PermissionEditDetails",
+                                     () -> getGroup().getPermissionEditDetails().name(),
+                                     this::setGroupPermissionEditDetails),
+                             new DbusProperty<>("PermissionSendMessage",
+                                     () -> getGroup().getPermissionSendMessage().name(),
+                                     this::setGroupPermissionSendMessage),
+                             new DbusProperty<>("GroupInviteLink", () -> {
+                                 final var groupInviteLinkUrl = getGroup().getGroupInviteLinkUrl();
+                                 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 | NotAGroupMemberException e) {
+                 throw new Error.GroupNotFound(e.getMessage());
+             } catch (IOException e) {
+                 throw new Error.Failure(e.getMessage());
+             } catch (LastGroupAdminException e) {
+                 throw new Error.LastGroupAdmin(e.getMessage());
+             }
+         }
+         @Override
+         public void addMembers(final List<String> recipients) throws Error.Failure {
+             final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
+             updateGroup(UpdateGroup.newBuilder().withMembers(memberIdentifiers).build());
+         }
+         @Override
+         public void removeMembers(final List<String> recipients) throws Error.Failure {
+             final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
+             updateGroup(UpdateGroup.newBuilder().withRemoveMembers(memberIdentifiers).build());
+         }
+         @Override
+         public void addAdmins(final List<String> recipients) throws Error.Failure {
+             final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
+             updateGroup(UpdateGroup.newBuilder().withAdmins(memberIdentifiers).build());
+         }
+         @Override
+         public void removeAdmins(final List<String> 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(new File(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.setGroupBlocked(groupId, isBlocked);
+             } catch (NotMasterDeviceException 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());
+             }
+         }
+     }
  }