]> nmode's Git Repositories - signal-cli/commitdiff
Implement new dbus group interface
authorAsamK <asamk@gmx.de>
Thu, 7 Oct 2021 19:18:14 +0000 (21:18 +0200)
committerAsamK <asamk@gmx.de>
Sat, 9 Oct 2021 15:41:23 +0000 (17:41 +0200)
15 files changed:
lib/src/main/java/org/asamk/signal/manager/Manager.java
lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
lib/src/main/java/org/asamk/signal/manager/api/Group.java
lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java
src/main/java/org/asamk/Signal.java
src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java
src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
src/main/java/org/asamk/signal/dbus/DbusProperty.java
src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java

index 7a4219660dd959c6af3cc14af8e5fafe77b182e1..f529b40865a75fbdebc8dfad985366aad9803464 100644 (file)
@@ -8,13 +8,12 @@ import org.asamk.signal.manager.api.RecipientIdentifier;
 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;
@@ -138,20 +137,7 @@ public interface Manager extends Closeable {
     ) 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(
index 0fd1eb33cf0d69a937f45ab6e52c4d0a91b2ccb8..8ccab36bbed56d62bbbc94bd5f888cb99f371eeb 100644 (file)
@@ -25,13 +25,12 @@ import org.asamk.signal.manager.api.RecipientIdentifier;
 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;
@@ -505,9 +504,12 @@ public class ManagerImpl implements Manager {
                         .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
@@ -532,35 +534,22 @@ public class ManagerImpl implements Manager {
 
     @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
index 650e10b6eeffcde5eee2fe45001fe4eba66743e7..4787ef950fb98c856bf7b718b904ebbb93adf25c 100644 (file)
@@ -2,6 +2,7 @@ package org.asamk.signal.manager.api;
 
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
 
 import java.util.Set;
@@ -17,9 +18,13 @@ public class Group {
     private final Set<RecipientAddress> requestingMembers;
     private final Set<RecipientAddress> adminMembers;
     private final boolean isBlocked;
-    private final int messageExpirationTime;
-    private final boolean isAnnouncementGroup;
+    private final int messageExpirationTimer;
+
+    private final GroupPermission permissionAddMember;
+    private final GroupPermission permissionEditDetails;
+    private final GroupPermission permissionSendMessage;
     private final boolean isMember;
+    private final boolean isAdmin;
 
     public Group(
             final GroupId groupId,
@@ -31,9 +36,12 @@ public class Group {
             final Set<RecipientAddress> requestingMembers,
             final Set<RecipientAddress> adminMembers,
             final boolean isBlocked,
-            final int messageExpirationTime,
-            final boolean isAnnouncementGroup,
-            final boolean isMember
+            final int messageExpirationTimer,
+            final GroupPermission permissionAddMember,
+            final GroupPermission permissionEditDetails,
+            final GroupPermission permissionSendMessage,
+            final boolean isMember,
+            final boolean isAdmin
     ) {
         this.groupId = groupId;
         this.title = title;
@@ -44,9 +52,12 @@ public class Group {
         this.requestingMembers = requestingMembers;
         this.adminMembers = adminMembers;
         this.isBlocked = isBlocked;
-        this.messageExpirationTime = messageExpirationTime;
-        this.isAnnouncementGroup = isAnnouncementGroup;
+        this.messageExpirationTimer = messageExpirationTimer;
+        this.permissionAddMember = permissionAddMember;
+        this.permissionEditDetails = permissionEditDetails;
+        this.permissionSendMessage = permissionSendMessage;
         this.isMember = isMember;
+        this.isAdmin = isAdmin;
     }
 
     public GroupId getGroupId() {
@@ -85,15 +96,27 @@ public class Group {
         return isBlocked;
     }
 
-    public int getMessageExpirationTime() {
-        return messageExpirationTime;
+    public int getMessageExpirationTimer() {
+        return messageExpirationTimer;
+    }
+
+    public GroupPermission getPermissionAddMember() {
+        return permissionAddMember;
     }
 
-    public boolean isAnnouncementGroup() {
-        return isAnnouncementGroup;
+    public GroupPermission getPermissionEditDetails() {
+        return permissionEditDetails;
+    }
+
+    public GroupPermission getPermissionSendMessage() {
+        return permissionSendMessage;
     }
 
     public boolean isMember() {
         return isMember;
     }
+
+    public boolean isAdmin() {
+        return isAdmin;
+    }
 }
diff --git a/lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java b/lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java
new file mode 100644 (file)
index 0000000..b5877ae
--- /dev/null
@@ -0,0 +1,203 @@
+package org.asamk.signal.manager.api;
+
+import org.asamk.signal.manager.groups.GroupLinkState;
+import org.asamk.signal.manager.groups.GroupPermission;
+
+import java.io.File;
+import java.util.Set;
+
+public class UpdateGroup {
+
+    private final String name;
+    private final String description;
+    private final Set<RecipientIdentifier.Single> members;
+    private final Set<RecipientIdentifier.Single> removeMembers;
+    private final Set<RecipientIdentifier.Single> admins;
+    private final Set<RecipientIdentifier.Single> removeAdmins;
+    private final boolean resetGroupLink;
+    private final GroupLinkState groupLinkState;
+    private final GroupPermission addMemberPermission;
+    private final GroupPermission editDetailsPermission;
+    private final File avatarFile;
+    private final Integer expirationTimer;
+    private final Boolean isAnnouncementGroup;
+
+    private UpdateGroup(final Builder builder) {
+        name = builder.name;
+        description = builder.description;
+        members = builder.members;
+        removeMembers = builder.removeMembers;
+        admins = builder.admins;
+        removeAdmins = builder.removeAdmins;
+        resetGroupLink = builder.resetGroupLink;
+        groupLinkState = builder.groupLinkState;
+        addMemberPermission = builder.addMemberPermission;
+        editDetailsPermission = builder.editDetailsPermission;
+        avatarFile = builder.avatarFile;
+        expirationTimer = builder.expirationTimer;
+        isAnnouncementGroup = builder.isAnnouncementGroup;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static Builder newBuilder(final UpdateGroup copy) {
+        Builder builder = new Builder();
+        builder.name = copy.getName();
+        builder.description = copy.getDescription();
+        builder.members = copy.getMembers();
+        builder.removeMembers = copy.getRemoveMembers();
+        builder.admins = copy.getAdmins();
+        builder.removeAdmins = copy.getRemoveAdmins();
+        builder.resetGroupLink = copy.isResetGroupLink();
+        builder.groupLinkState = copy.getGroupLinkState();
+        builder.addMemberPermission = copy.getAddMemberPermission();
+        builder.editDetailsPermission = copy.getEditDetailsPermission();
+        builder.avatarFile = copy.getAvatarFile();
+        builder.expirationTimer = copy.getExpirationTimer();
+        builder.isAnnouncementGroup = copy.getIsAnnouncementGroup();
+        return builder;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public Set<RecipientIdentifier.Single> getMembers() {
+        return members;
+    }
+
+    public Set<RecipientIdentifier.Single> getRemoveMembers() {
+        return removeMembers;
+    }
+
+    public Set<RecipientIdentifier.Single> getAdmins() {
+        return admins;
+    }
+
+    public Set<RecipientIdentifier.Single> getRemoveAdmins() {
+        return removeAdmins;
+    }
+
+    public boolean isResetGroupLink() {
+        return resetGroupLink;
+    }
+
+    public GroupLinkState getGroupLinkState() {
+        return groupLinkState;
+    }
+
+    public GroupPermission getAddMemberPermission() {
+        return addMemberPermission;
+    }
+
+    public GroupPermission getEditDetailsPermission() {
+        return editDetailsPermission;
+    }
+
+    public File getAvatarFile() {
+        return avatarFile;
+    }
+
+    public Integer getExpirationTimer() {
+        return expirationTimer;
+    }
+
+    public Boolean getIsAnnouncementGroup() {
+        return isAnnouncementGroup;
+    }
+
+    public static final class Builder {
+
+        private String name;
+        private String description;
+        private Set<RecipientIdentifier.Single> members;
+        private Set<RecipientIdentifier.Single> removeMembers;
+        private Set<RecipientIdentifier.Single> admins;
+        private Set<RecipientIdentifier.Single> removeAdmins;
+        private boolean resetGroupLink;
+        private GroupLinkState groupLinkState;
+        private GroupPermission addMemberPermission;
+        private GroupPermission editDetailsPermission;
+        private File avatarFile;
+        private Integer expirationTimer;
+        private Boolean isAnnouncementGroup;
+
+        private Builder() {
+        }
+
+        public Builder withName(final String val) {
+            name = val;
+            return this;
+        }
+
+        public Builder withDescription(final String val) {
+            description = val;
+            return this;
+        }
+
+        public Builder withMembers(final Set<RecipientIdentifier.Single> val) {
+            members = val;
+            return this;
+        }
+
+        public Builder withRemoveMembers(final Set<RecipientIdentifier.Single> val) {
+            removeMembers = val;
+            return this;
+        }
+
+        public Builder withAdmins(final Set<RecipientIdentifier.Single> val) {
+            admins = val;
+            return this;
+        }
+
+        public Builder withRemoveAdmins(final Set<RecipientIdentifier.Single> val) {
+            removeAdmins = val;
+            return this;
+        }
+
+        public Builder withResetGroupLink(final boolean val) {
+            resetGroupLink = val;
+            return this;
+        }
+
+        public Builder withGroupLinkState(final GroupLinkState val) {
+            groupLinkState = val;
+            return this;
+        }
+
+        public Builder withAddMemberPermission(final GroupPermission val) {
+            addMemberPermission = val;
+            return this;
+        }
+
+        public Builder withEditDetailsPermission(final GroupPermission val) {
+            editDetailsPermission = val;
+            return this;
+        }
+
+        public Builder withAvatarFile(final File val) {
+            avatarFile = val;
+            return this;
+        }
+
+        public Builder withExpirationTimer(final Integer val) {
+            expirationTimer = val;
+            return this;
+        }
+
+        public Builder withIsAnnouncementGroup(final Boolean val) {
+            isAnnouncementGroup = val;
+            return this;
+        }
+
+        public UpdateGroup build() {
+            return new UpdateGroup(this);
+        }
+    }
+}
index 62f4f11134b7169f747c24a522129c08e708c570..ee2e94166ccbb55b23c7cb33402038d7f22924cd 100644 (file)
@@ -639,7 +639,7 @@ public class GroupHelper {
 
         return SignalServiceDataMessage.newBuilder()
                 .asGroupMessage(group.build())
-                .withExpiration(g.getMessageExpirationTime());
+                .withExpiration(g.getMessageExpirationTimer());
     }
 
     private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
@@ -648,7 +648,7 @@ public class GroupHelper {
                 .withSignedGroupChange(signedGroupChange);
         return SignalServiceDataMessage.newBuilder()
                 .asGroupMessage(group.build())
-                .withExpiration(g.getMessageExpirationTime());
+                .withExpiration(g.getMessageExpirationTimer());
     }
 
     private SendGroupMessageResults sendUpdateGroupV2Message(
index c0953f1fa27be3deb94f0a95fb3a13cb32bb4670..6c0fb2e9cbde4d3974679f00e7370d0317f6ab39 100644 (file)
@@ -100,7 +100,7 @@ public class SendHelper {
             final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g
     ) throws IOException, GroupSendingNotAllowedException {
         GroupUtils.setGroupContext(messageBuilder, g);
-        messageBuilder.withExpiration(g.getMessageExpirationTime());
+        messageBuilder.withExpiration(g.getMessageExpirationTimer());
 
         final var message = messageBuilder.build();
         final var recipients = g.getMembersWithout(account.getSelfRecipientId());
index 60efc84b2df4baf54efef4abd66317f10d1a1481..b4f4e63a6cba5fd9a3522667bae29c655f281131 100644 (file)
@@ -2,6 +2,7 @@ package org.asamk.signal.manager.storage.groups;
 
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
 
 import java.util.Set;
@@ -38,10 +39,16 @@ public abstract class GroupInfo {
 
     public abstract void setBlocked(boolean blocked);
 
-    public abstract int getMessageExpirationTime();
+    public abstract int getMessageExpirationTimer();
 
     public abstract boolean isAnnouncementGroup();
 
+    public abstract GroupPermission getPermissionAddMember();
+
+    public abstract GroupPermission getPermissionEditDetails();
+
+    public abstract GroupPermission getPermissionSendMessage();
+
     public Set<RecipientId> getMembersWithout(RecipientId recipientId) {
         return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet());
     }
index 49c9a5042b34187fa17df91705a8dae71a3211f7..dbd2dcbba873422296a4ed8f78e803cba297947f 100644 (file)
@@ -3,6 +3,7 @@ package org.asamk.signal.manager.storage.groups;
 import org.asamk.signal.manager.groups.GroupIdV1;
 import org.asamk.signal.manager.groups.GroupIdV2;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.groups.GroupUtils;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
 
@@ -85,7 +86,7 @@ public class GroupInfoV1 extends GroupInfo {
     }
 
     @Override
-    public int getMessageExpirationTime() {
+    public int getMessageExpirationTimer() {
         return messageExpirationTime;
     }
 
@@ -94,6 +95,21 @@ public class GroupInfoV1 extends GroupInfo {
         return false;
     }
 
+    @Override
+    public GroupPermission getPermissionAddMember() {
+        return GroupPermission.EVERY_MEMBER;
+    }
+
+    @Override
+    public GroupPermission getPermissionEditDetails() {
+        return GroupPermission.EVERY_MEMBER;
+    }
+
+    @Override
+    public GroupPermission getPermissionSendMessage() {
+        return GroupPermission.EVERY_MEMBER;
+    }
+
     public void addMembers(Collection<RecipientId> members) {
         this.members.addAll(members);
     }
index a06b83dfa3203d064533266429c1d92dcfac91ee..34db2a2879ecb66590bb95715c51238935688daf 100644 (file)
@@ -2,6 +2,7 @@ package org.asamk.signal.manager.storage.groups;
 
 import org.asamk.signal.manager.groups.GroupIdV2;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.asamk.signal.manager.storage.recipients.RecipientResolver;
 import org.signal.storageservice.protos.groups.AccessControl;
@@ -151,7 +152,7 @@ public class GroupInfoV2 extends GroupInfo {
     }
 
     @Override
-    public int getMessageExpirationTime() {
+    public int getMessageExpirationTimer() {
         return this.group != null && this.group.hasDisappearingMessagesTimer()
                 ? this.group.getDisappearingMessagesTimer().getDuration()
                 : 0;
@@ -162,6 +163,23 @@ public class GroupInfoV2 extends GroupInfo {
         return this.group != null && this.group.getIsAnnouncementGroup() == EnabledState.ENABLED;
     }
 
+    @Override
+    public GroupPermission getPermissionAddMember() {
+        final var accessControl = getAccessControl();
+        return accessControl == null ? GroupPermission.EVERY_MEMBER : toGroupPermission(accessControl.getMembers());
+    }
+
+    @Override
+    public GroupPermission getPermissionEditDetails() {
+        final var accessControl = getAccessControl();
+        return accessControl == null ? GroupPermission.EVERY_MEMBER : toGroupPermission(accessControl.getAttributes());
+    }
+
+    @Override
+    public GroupPermission getPermissionSendMessage() {
+        return isAnnouncementGroup() ? GroupPermission.ONLY_ADMINS : GroupPermission.EVERY_MEMBER;
+    }
+
     public void setPermissionDenied(final boolean permissionDenied) {
         this.permissionDenied = permissionDenied;
     }
@@ -169,4 +187,22 @@ public class GroupInfoV2 extends GroupInfo {
     public boolean isPermissionDenied() {
         return permissionDenied;
     }
+
+    private AccessControl getAccessControl() {
+        if (this.group == null || !this.group.hasAccessControl()) {
+            return null;
+        }
+
+        return this.group.getAccessControl();
+    }
+
+    private static GroupPermission toGroupPermission(final AccessControl.AccessRequired permission) {
+        switch (permission) {
+            case ADMINISTRATOR:
+                return GroupPermission.ONLY_ADMINS;
+            case MEMBER:
+            default:
+                return GroupPermission.EVERY_MEMBER;
+        }
+    }
 }
index bf8265ffe933eed6083e9d569322d8b6d0c7f1c4..349671b37bffb05e9fa058862046adb8b2afd6af 100644 (file)
@@ -1,7 +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 @@ public interface Signal extends DBusInterface {
 
     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;
@@ -133,12 +145,15 @@ public interface Signal extends DBusInterface {
 
     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;
@@ -303,6 +318,71 @@ public interface Signal extends DBusInterface {
         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 {
@@ -347,6 +427,13 @@ public interface Signal extends DBusInterface {
             }
         }
 
+        class LastGroupAdmin extends DBusExecutionException {
+
+            public LastGroupAdmin(final String message) {
+                super(message);
+            }
+        }
+
         class InvalidNumber extends DBusExecutionException {
 
             public InvalidNumber(final String message) {
index fd8c4b92141bdeb838276c25aa16a31fc82e63dd..b2182429d30648de5de762149d5ef50bcc2e80e9 100644 (file)
@@ -63,7 +63,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
                     resolveMembers(group.getPendingMembers()),
                     resolveMembers(group.getRequestingMembers()),
                     resolveMembers(group.getAdminMembers()),
-                    group.getMessageExpirationTime() == 0 ? "disabled" : group.getMessageExpirationTime() + "s",
+                    group.getMessageExpirationTimer() == 0 ? "disabled" : group.getMessageExpirationTimer() + "s",
                     groupInviteLink == null ? '-' : groupInviteLink.getUrl());
         } else {
             writer.println("Id: {} Name: {}  Active: {} Blocked: {}",
@@ -91,11 +91,14 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
                         group.getDescription(),
                         group.isMember(),
                         group.isBlocked(),
-                        group.getMessageExpirationTime(),
+                        group.getMessageExpirationTimer(),
                         resolveJsonMembers(group.getMembers()),
                         resolveJsonMembers(group.getPendingMembers()),
                         resolveJsonMembers(group.getRequestingMembers()),
                         resolveJsonMembers(group.getAdminMembers()),
+                        group.getPermissionAddMember().name(),
+                        group.getPermissionEditDetails().name(),
+                        group.getPermissionSendMessage().name(),
                         groupInviteLink == null ? null : groupInviteLink.getUrl());
             }).collect(Collectors.toList());
 
@@ -122,6 +125,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
         public final Set<JsonGroupMember> pendingMembers;
         public final Set<JsonGroupMember> requestingMembers;
         public final Set<JsonGroupMember> admins;
+        public final String permissionAddMember;
+        public final String permissionEditDetails;
+        public final String permissionSendMessage;
         public final String groupInviteLink;
 
         public JsonGroup(
@@ -135,6 +141,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
                 Set<JsonGroupMember> pendingMembers,
                 Set<JsonGroupMember> requestingMembers,
                 Set<JsonGroupMember> admins,
+                final String permissionAddMember,
+                final String permissionEditDetails,
+                final String permissionSendMessage,
                 String groupInviteLink
         ) {
             this.id = id;
@@ -148,6 +157,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
             this.pendingMembers = pendingMembers;
             this.requestingMembers = requestingMembers;
             this.admins = admins;
+            this.permissionAddMember = permissionAddMember;
+            this.permissionEditDetails = permissionEditDetails;
+            this.permissionSendMessage = permissionSendMessage;
             this.groupInviteLink = groupInviteLink;
         }
     }
index 68bce2d222a0a3756e3b4635aad57517cc5554c9..b63a7160c509f80bfaedfb2a18e824bea5eba410 100644 (file)
@@ -12,6 +12,7 @@ import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
 import org.asamk.signal.commands.exceptions.UserErrorException;
 import org.asamk.signal.manager.AttachmentInvalidException;
 import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.api.UpdateGroup;
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupLinkState;
 import org.asamk.signal.manager.groups.GroupNotFoundException;
@@ -145,21 +146,23 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand {
             }
 
             var results = m.updateGroup(groupId,
-                    groupName,
-                    groupDescription,
-                    groupMembers,
-                    groupRemoveMembers,
-                    groupAdmins,
-                    groupRemoveAdmins,
-                    groupResetLink,
-                    groupLinkState,
-                    groupAddMemberPermission,
-                    groupEditDetailsPermission,
-                    groupAvatar == null ? null : new File(groupAvatar),
-                    groupExpiration,
-                    groupSendMessagesPermission == null
-                            ? null
-                            : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS);
+                    UpdateGroup.newBuilder()
+                            .withName(groupName)
+                            .withDescription(groupDescription)
+                            .withMembers(groupMembers)
+                            .withRemoveMembers(groupRemoveMembers)
+                            .withAdmins(groupAdmins)
+                            .withRemoveAdmins(groupRemoveAdmins)
+                            .withResetGroupLink(groupResetLink)
+                            .withGroupLinkState(groupLinkState)
+                            .withAddMemberPermission(groupAddMemberPermission)
+                            .withEditDetailsPermission(groupEditDetailsPermission)
+                            .withAvatarFile(groupAvatar == null ? null : new File(groupAvatar))
+                            .withExpirationTimer(groupExpiration)
+                            .withIsAnnouncementGroup(groupSendMessagesPermission == null
+                                    ? null
+                                    : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS)
+                            .build());
             if (results != null) {
                 timestamp = results.getTimestamp();
                 ErrorUtils.handleSendMessageResults(results.getResults());
index 53148c01c54199f9c375f2f2099553c07b086c9d..6e655bdc3ec8f2f3591983526d03b0e7e470eb63 100644 (file)
@@ -15,9 +15,9 @@ import org.asamk.signal.manager.api.RecipientIdentifier;
 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;
@@ -183,8 +183,8 @@ public class DbusManagerImpl implements Manager {
 
     @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
@@ -194,7 +194,8 @@ public class DbusManagerImpl implements Manager {
         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());
     }
 
@@ -207,8 +208,7 @@ public class DbusManagerImpl implements Manager {
     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()));
@@ -216,25 +216,76 @@ public class DbusManagerImpl implements Manager {
 
     @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());
     }
 
@@ -344,7 +395,12 @@ public class DbusManagerImpl implements Manager {
     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
@@ -411,19 +467,41 @@ public class DbusManagerImpl implements Manager {
 
     @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
index e055778699b63ba7bf98c85e57733b7a230b4276..5042458ebd269198e7f401693dbffb30a08b30e8 100644 (file)
@@ -21,6 +21,12 @@ public class DbusProperty<T> {
         this.setter = null;
     }
 
+    public DbusProperty(final String name, final Consumer<T> setter) {
+        this.name = name;
+        this.getter = null;
+        this.setter = setter;
+    }
+
     public String getName() {
         return name;
     }
index ab19f0cec85116986de85e60184aeed8e8a5c290..56ecdc55a9b3a6b5c91b0487f65e0baaae665dce 100644 (file)
@@ -1,7 +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 @@ import org.asamk.signal.manager.api.Identity;
 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 @@ 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 @@ 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 @@ public class DbusSignalImpl implements Signal {
 
     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 @@ public class DbusSignalImpl implements Signal {
 
     public void initObjects() {
         updateDevices();
+        updateGroups();
     }
 
     public void close() {
@@ -415,6 +422,22 @@ public class DbusSignalImpl implements Signal {
         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));
@@ -431,10 +454,18 @@ public class DbusSignalImpl implements Signal {
         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 {
@@ -448,19 +479,11 @@ public class DbusSignalImpl implements Signal {
                 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());
                 }
@@ -740,6 +763,10 @@ public class DbusSignalImpl implements Signal {
         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 {
@@ -817,6 +844,38 @@ public class DbusSignalImpl implements Signal {
         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;
@@ -858,4 +917,166 @@ public class DbusSignalImpl implements Signal {
             }
         }
     }
+
+    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 (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());
+            }
+        }
+    }
 }