Implement new dbus group interface

This commit is contained in:
AsamK 2021-10-07 21:18:14 +02:00
parent b5d4a5000b
commit 997b4f0c3f
15 changed files with 803 additions and 136 deletions

View 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.SendGroupMessageResults;
import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.SendMessageResults;
import org.asamk.signal.manager.api.TypingAction; 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.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironment; import org.asamk.signal.manager.config.ServiceEnvironment;
import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl; 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.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.groups.NotAGroupMemberException;
@ -138,20 +137,7 @@ public interface Manager extends Closeable {
) throws IOException, AttachmentInvalidException; ) throws IOException, AttachmentInvalidException;
SendGroupMessageResults updateGroup( SendGroupMessageResults updateGroup(
GroupId groupId, final GroupId groupId, final UpdateGroup updateGroup
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
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException; ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException;
Pair<GroupId, SendGroupMessageResults> joinGroup( Pair<GroupId, SendGroupMessageResults> joinGroup(

View 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.SendGroupMessageResults;
import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.SendMessageResults;
import org.asamk.signal.manager.api.TypingAction; 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.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl; 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.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.groups.NotAGroupMemberException;
@ -505,9 +504,12 @@ public class ManagerImpl implements Manager {
.map(account.getRecipientStore()::resolveRecipientAddress) .map(account.getRecipientStore()::resolveRecipientAddress)
.collect(Collectors.toSet()), .collect(Collectors.toSet()),
groupInfo.isBlocked(), groupInfo.isBlocked(),
groupInfo.getMessageExpirationTime(), groupInfo.getMessageExpirationTimer(),
groupInfo.isAnnouncementGroup(), groupInfo.getPermissionAddMember(),
groupInfo.isMember(account.getSelfRecipientId())); groupInfo.getPermissionEditDetails(),
groupInfo.getPermissionSendMessage(),
groupInfo.isMember(account.getSelfRecipientId()),
groupInfo.isAdmin(account.getSelfRecipientId()));
} }
@Override @Override
@ -532,35 +534,22 @@ public class ManagerImpl implements Manager {
@Override @Override
public SendGroupMessageResults updateGroup( public SendGroupMessageResults updateGroup(
GroupId groupId, final GroupId groupId, final UpdateGroup updateGroup
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
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
return groupHelper.updateGroup(groupId, return groupHelper.updateGroup(groupId,
name, updateGroup.getName(),
description, updateGroup.getDescription(),
members == null ? null : resolveRecipients(members), updateGroup.getMembers() == null ? null : resolveRecipients(updateGroup.getMembers()),
removeMembers == null ? null : resolveRecipients(removeMembers), updateGroup.getRemoveMembers() == null ? null : resolveRecipients(updateGroup.getRemoveMembers()),
admins == null ? null : resolveRecipients(admins), updateGroup.getAdmins() == null ? null : resolveRecipients(updateGroup.getAdmins()),
removeAdmins == null ? null : resolveRecipients(removeAdmins), updateGroup.getRemoveAdmins() == null ? null : resolveRecipients(updateGroup.getRemoveAdmins()),
resetGroupLink, updateGroup.isResetGroupLink(),
groupLinkState, updateGroup.getGroupLinkState(),
addMemberPermission, updateGroup.getAddMemberPermission(),
editDetailsPermission, updateGroup.getEditDetailsPermission(),
avatarFile, updateGroup.getAvatarFile(),
expirationTimer, updateGroup.getExpirationTimer(),
isAnnouncementGroup); updateGroup.getIsAnnouncementGroup());
} }
@Override @Override

View file

@ -2,6 +2,7 @@ package org.asamk.signal.manager.api;
import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import java.util.Set; import java.util.Set;
@ -17,9 +18,13 @@ public class Group {
private final Set<RecipientAddress> requestingMembers; private final Set<RecipientAddress> requestingMembers;
private final Set<RecipientAddress> adminMembers; private final Set<RecipientAddress> adminMembers;
private final boolean isBlocked; private final boolean isBlocked;
private final int messageExpirationTime; private final int messageExpirationTimer;
private final boolean isAnnouncementGroup;
private final GroupPermission permissionAddMember;
private final GroupPermission permissionEditDetails;
private final GroupPermission permissionSendMessage;
private final boolean isMember; private final boolean isMember;
private final boolean isAdmin;
public Group( public Group(
final GroupId groupId, final GroupId groupId,
@ -31,9 +36,12 @@ public class Group {
final Set<RecipientAddress> requestingMembers, final Set<RecipientAddress> requestingMembers,
final Set<RecipientAddress> adminMembers, final Set<RecipientAddress> adminMembers,
final boolean isBlocked, final boolean isBlocked,
final int messageExpirationTime, final int messageExpirationTimer,
final boolean isAnnouncementGroup, final GroupPermission permissionAddMember,
final boolean isMember final GroupPermission permissionEditDetails,
final GroupPermission permissionSendMessage,
final boolean isMember,
final boolean isAdmin
) { ) {
this.groupId = groupId; this.groupId = groupId;
this.title = title; this.title = title;
@ -44,9 +52,12 @@ public class Group {
this.requestingMembers = requestingMembers; this.requestingMembers = requestingMembers;
this.adminMembers = adminMembers; this.adminMembers = adminMembers;
this.isBlocked = isBlocked; this.isBlocked = isBlocked;
this.messageExpirationTime = messageExpirationTime; this.messageExpirationTimer = messageExpirationTimer;
this.isAnnouncementGroup = isAnnouncementGroup; this.permissionAddMember = permissionAddMember;
this.permissionEditDetails = permissionEditDetails;
this.permissionSendMessage = permissionSendMessage;
this.isMember = isMember; this.isMember = isMember;
this.isAdmin = isAdmin;
} }
public GroupId getGroupId() { public GroupId getGroupId() {
@ -85,15 +96,27 @@ public class Group {
return isBlocked; return isBlocked;
} }
public int getMessageExpirationTime() { public int getMessageExpirationTimer() {
return messageExpirationTime; return messageExpirationTimer;
} }
public boolean isAnnouncementGroup() { public GroupPermission getPermissionAddMember() {
return isAnnouncementGroup; return permissionAddMember;
}
public GroupPermission getPermissionEditDetails() {
return permissionEditDetails;
}
public GroupPermission getPermissionSendMessage() {
return permissionSendMessage;
} }
public boolean isMember() { public boolean isMember() {
return isMember; return isMember;
} }
public boolean isAdmin() {
return isAdmin;
}
} }

View file

@ -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);
}
}
}

View file

@ -639,7 +639,7 @@ public class GroupHelper {
return SignalServiceDataMessage.newBuilder() return SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build()) .asGroupMessage(group.build())
.withExpiration(g.getMessageExpirationTime()); .withExpiration(g.getMessageExpirationTimer());
} }
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
@ -648,7 +648,7 @@ public class GroupHelper {
.withSignedGroupChange(signedGroupChange); .withSignedGroupChange(signedGroupChange);
return SignalServiceDataMessage.newBuilder() return SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build()) .asGroupMessage(group.build())
.withExpiration(g.getMessageExpirationTime()); .withExpiration(g.getMessageExpirationTimer());
} }
private SendGroupMessageResults sendUpdateGroupV2Message( private SendGroupMessageResults sendUpdateGroupV2Message(

View file

@ -100,7 +100,7 @@ public class SendHelper {
final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g
) throws IOException, GroupSendingNotAllowedException { ) throws IOException, GroupSendingNotAllowedException {
GroupUtils.setGroupContext(messageBuilder, g); GroupUtils.setGroupContext(messageBuilder, g);
messageBuilder.withExpiration(g.getMessageExpirationTime()); messageBuilder.withExpiration(g.getMessageExpirationTimer());
final var message = messageBuilder.build(); final var message = messageBuilder.build();
final var recipients = g.getMembersWithout(account.getSelfRecipientId()); final var recipients = g.getMembersWithout(account.getSelfRecipientId());

View 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.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl; 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.RecipientId;
import java.util.Set; import java.util.Set;
@ -38,10 +39,16 @@ public abstract class GroupInfo {
public abstract void setBlocked(boolean blocked); public abstract void setBlocked(boolean blocked);
public abstract int getMessageExpirationTime(); public abstract int getMessageExpirationTimer();
public abstract boolean isAnnouncementGroup(); public abstract boolean isAnnouncementGroup();
public abstract GroupPermission getPermissionAddMember();
public abstract GroupPermission getPermissionEditDetails();
public abstract GroupPermission getPermissionSendMessage();
public Set<RecipientId> getMembersWithout(RecipientId recipientId) { public Set<RecipientId> getMembersWithout(RecipientId recipientId) {
return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet()); return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet());
} }

View 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.GroupIdV1;
import org.asamk.signal.manager.groups.GroupIdV2; import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl; 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.groups.GroupUtils;
import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientId;
@ -85,7 +86,7 @@ public class GroupInfoV1 extends GroupInfo {
} }
@Override @Override
public int getMessageExpirationTime() { public int getMessageExpirationTimer() {
return messageExpirationTime; return messageExpirationTime;
} }
@ -94,6 +95,21 @@ public class GroupInfoV1 extends GroupInfo {
return false; 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) { public void addMembers(Collection<RecipientId> members) {
this.members.addAll(members); this.members.addAll(members);
} }

View 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.GroupIdV2;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl; 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.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.AccessControl;
@ -151,7 +152,7 @@ public class GroupInfoV2 extends GroupInfo {
} }
@Override @Override
public int getMessageExpirationTime() { public int getMessageExpirationTimer() {
return this.group != null && this.group.hasDisappearingMessagesTimer() return this.group != null && this.group.hasDisappearingMessagesTimer()
? this.group.getDisappearingMessagesTimer().getDuration() ? this.group.getDisappearingMessagesTimer().getDuration()
: 0; : 0;
@ -162,6 +163,23 @@ public class GroupInfoV2 extends GroupInfo {
return this.group != null && this.group.getIsAnnouncementGroup() == EnabledState.ENABLED; 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) { public void setPermissionDenied(final boolean permissionDenied) {
this.permissionDenied = permissionDenied; this.permissionDenied = permissionDenied;
} }
@ -169,4 +187,22 @@ public class GroupInfoV2 extends GroupInfo {
public boolean isPermissionDenied() { public boolean isPermissionDenied() {
return permissionDenied; 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;
}
}
} }

View file

@ -1,7 +1,6 @@
package org.asamk; package org.asamk;
import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.commands.exceptions.IOErrorException;
import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.DBusPath;
import org.freedesktop.dbus.Struct; import org.freedesktop.dbus.Struct;
import org.freedesktop.dbus.annotations.DBusProperty; import org.freedesktop.dbus.annotations.DBusProperty;
@ -84,14 +83,27 @@ public interface Signal extends DBusInterface {
void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber; void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber;
@Deprecated
void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound, Error.InvalidGroupId; void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound, Error.InvalidGroupId;
@Deprecated
List<byte[]> getGroupIds(); List<byte[]> getGroupIds();
DBusPath getGroup(byte[] groupId);
List<StructGroup> listGroups();
@Deprecated
String getGroupName(byte[] groupId) throws Error.InvalidGroupId; String getGroupName(byte[] groupId) throws Error.InvalidGroupId;
@Deprecated
List<String> getGroupMembers(byte[] groupId) throws Error.InvalidGroupId; 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[] updateGroup(
byte[] groupId, String name, List<String> members, String avatar byte[] groupId, String name, List<String> members, String avatar
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.InvalidGroupId; ) 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; List<String> getContactNumber(final String name) throws Error.Failure;
@Deprecated
void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure, Error.InvalidGroupId; void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure, Error.InvalidGroupId;
boolean isContactBlocked(final String number) throws Error.InvalidNumber; boolean isContactBlocked(final String number) throws Error.InvalidNumber;
@Deprecated
boolean isGroupBlocked(final byte[] groupId) throws Error.InvalidGroupId; boolean isGroupBlocked(final byte[] groupId) throws Error.InvalidGroupId;
@Deprecated
boolean isMember(final byte[] groupId) throws Error.InvalidGroupId; boolean isMember(final byte[] groupId) throws Error.InvalidGroupId;
byte[] joinGroup(final String groupLink) throws Error.Failure; byte[] joinGroup(final String groupLink) throws Error.Failure;
@ -303,6 +318,71 @@ public interface Signal extends DBusInterface {
void removeDevice() throws Error.Failure; 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 { interface Error {
class AttachmentInvalid extends DBusExecutionException { 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 { class InvalidNumber extends DBusExecutionException {
public InvalidNumber(final String message) { public InvalidNumber(final String message) {

View file

@ -63,7 +63,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
resolveMembers(group.getPendingMembers()), resolveMembers(group.getPendingMembers()),
resolveMembers(group.getRequestingMembers()), resolveMembers(group.getRequestingMembers()),
resolveMembers(group.getAdminMembers()), resolveMembers(group.getAdminMembers()),
group.getMessageExpirationTime() == 0 ? "disabled" : group.getMessageExpirationTime() + "s", group.getMessageExpirationTimer() == 0 ? "disabled" : group.getMessageExpirationTimer() + "s",
groupInviteLink == null ? '-' : groupInviteLink.getUrl()); groupInviteLink == null ? '-' : groupInviteLink.getUrl());
} else { } else {
writer.println("Id: {} Name: {} Active: {} Blocked: {}", writer.println("Id: {} Name: {} Active: {} Blocked: {}",
@ -91,11 +91,14 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
group.getDescription(), group.getDescription(),
group.isMember(), group.isMember(),
group.isBlocked(), group.isBlocked(),
group.getMessageExpirationTime(), group.getMessageExpirationTimer(),
resolveJsonMembers(group.getMembers()), resolveJsonMembers(group.getMembers()),
resolveJsonMembers(group.getPendingMembers()), resolveJsonMembers(group.getPendingMembers()),
resolveJsonMembers(group.getRequestingMembers()), resolveJsonMembers(group.getRequestingMembers()),
resolveJsonMembers(group.getAdminMembers()), resolveJsonMembers(group.getAdminMembers()),
group.getPermissionAddMember().name(),
group.getPermissionEditDetails().name(),
group.getPermissionSendMessage().name(),
groupInviteLink == null ? null : groupInviteLink.getUrl()); groupInviteLink == null ? null : groupInviteLink.getUrl());
}).collect(Collectors.toList()); }).collect(Collectors.toList());
@ -122,6 +125,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
public final Set<JsonGroupMember> pendingMembers; public final Set<JsonGroupMember> pendingMembers;
public final Set<JsonGroupMember> requestingMembers; public final Set<JsonGroupMember> requestingMembers;
public final Set<JsonGroupMember> admins; public final Set<JsonGroupMember> admins;
public final String permissionAddMember;
public final String permissionEditDetails;
public final String permissionSendMessage;
public final String groupInviteLink; public final String groupInviteLink;
public JsonGroup( public JsonGroup(
@ -135,6 +141,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
Set<JsonGroupMember> pendingMembers, Set<JsonGroupMember> pendingMembers,
Set<JsonGroupMember> requestingMembers, Set<JsonGroupMember> requestingMembers,
Set<JsonGroupMember> admins, Set<JsonGroupMember> admins,
final String permissionAddMember,
final String permissionEditDetails,
final String permissionSendMessage,
String groupInviteLink String groupInviteLink
) { ) {
this.id = id; this.id = id;
@ -148,6 +157,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
this.pendingMembers = pendingMembers; this.pendingMembers = pendingMembers;
this.requestingMembers = requestingMembers; this.requestingMembers = requestingMembers;
this.admins = admins; this.admins = admins;
this.permissionAddMember = permissionAddMember;
this.permissionEditDetails = permissionEditDetails;
this.permissionSendMessage = permissionSendMessage;
this.groupInviteLink = groupInviteLink; this.groupInviteLink = groupInviteLink;
} }
} }

View file

@ -12,6 +12,7 @@ import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.AttachmentInvalidException;
import org.asamk.signal.manager.Manager; 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.GroupId;
import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupLinkState;
import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupNotFoundException;
@ -145,21 +146,23 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand {
} }
var results = m.updateGroup(groupId, var results = m.updateGroup(groupId,
groupName, UpdateGroup.newBuilder()
groupDescription, .withName(groupName)
groupMembers, .withDescription(groupDescription)
groupRemoveMembers, .withMembers(groupMembers)
groupAdmins, .withRemoveMembers(groupRemoveMembers)
groupRemoveAdmins, .withAdmins(groupAdmins)
groupResetLink, .withRemoveAdmins(groupRemoveAdmins)
groupLinkState, .withResetGroupLink(groupResetLink)
groupAddMemberPermission, .withGroupLinkState(groupLinkState)
groupEditDetailsPermission, .withAddMemberPermission(groupAddMemberPermission)
groupAvatar == null ? null : new File(groupAvatar), .withEditDetailsPermission(groupEditDetailsPermission)
groupExpiration, .withAvatarFile(groupAvatar == null ? null : new File(groupAvatar))
groupSendMessagesPermission == null .withExpirationTimer(groupExpiration)
.withIsAnnouncementGroup(groupSendMessagesPermission == null
? null ? null
: groupSendMessagesPermission == GroupPermission.ONLY_ADMINS); : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS)
.build());
if (results != null) { if (results != null) {
timestamp = results.getTimestamp(); timestamp = results.getTimestamp();
ErrorUtils.handleSendMessageResults(results.getResults()); ErrorUtils.handleSendMessageResults(results.getResults());

View 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.SendGroupMessageResults;
import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.SendMessageResults;
import org.asamk.signal.manager.api.TypingAction; 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.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl; 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.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
@ -183,8 +183,8 @@ public class DbusManagerImpl implements Manager {
@Override @Override
public List<Group> getGroups() { public List<Group> getGroups() {
final var groupIds = signal.getGroupIds(); final var groups = signal.listGroups();
return groupIds.stream().map(id -> getGroup(GroupId.unknownVersion(id))).collect(Collectors.toList()); return groups.stream().map(Signal.StructGroup::getObjectPath).map(this::getGroup).collect(Collectors.toList());
} }
@Override @Override
@ -194,7 +194,8 @@ public class DbusManagerImpl implements Manager {
if (groupAdmins.size() > 0) { if (groupAdmins.size() > 0) {
throw new UnsupportedOperationException(); 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()); return new SendGroupMessageResults(0, List.of());
} }
@ -207,8 +208,7 @@ public class DbusManagerImpl implements Manager {
public Pair<GroupId, SendGroupMessageResults> createGroup( public Pair<GroupId, SendGroupMessageResults> createGroup(
final String name, final Set<RecipientIdentifier.Single> members, final File avatarFile final String name, final Set<RecipientIdentifier.Single> members, final File avatarFile
) throws IOException, AttachmentInvalidException { ) throws IOException, AttachmentInvalidException {
final var newGroupId = signal.updateGroup(new byte[0], final var newGroupId = signal.createGroup(emptyIfNull(name),
emptyIfNull(name),
members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()), members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()),
avatarFile == null ? "" : avatarFile.getPath()); avatarFile == null ? "" : avatarFile.getPath());
return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of())); return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
@ -216,25 +216,76 @@ public class DbusManagerImpl implements Manager {
@Override @Override
public SendGroupMessageResults updateGroup( public SendGroupMessageResults updateGroup(
final GroupId groupId, final GroupId groupId, final UpdateGroup updateGroup
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
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
signal.updateGroup(groupId.serialize(), final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
emptyIfNull(name), if (updateGroup.getName() != null) {
members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()), group.Set("org.asamk.Signal.Group", "Name", updateGroup.getName());
avatarFile == null ? "" : avatarFile.getPath()); }
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()); return new SendGroupMessageResults(0, List.of());
} }
@ -344,7 +395,12 @@ public class DbusManagerImpl implements Manager {
public void setGroupBlocked( public void setGroupBlocked(
final GroupId groupId, final boolean blocked final GroupId groupId, final boolean blocked
) throws GroupNotFoundException, IOException { ) 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
@ -411,19 +467,41 @@ public class DbusManagerImpl implements Manager {
@Override @Override
public Group getGroup(final GroupId groupId) { public Group getGroup(final GroupId groupId) {
final var id = groupId.serialize(); final var groupPath = signal.getGroup(groupId.serialize());
return new Group(groupId, return getGroup(groupPath);
signal.getGroupName(id), }
null,
null, @SuppressWarnings("unchecked")
signal.getGroupMembers(id).stream().map(m -> new RecipientAddress(null, m)).collect(Collectors.toSet()), private Group getGroup(final DBusPath groupPath) {
Set.of(), final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
Set.of(), final var id = (byte[]) group.get("Id").getValue();
Set.of(), try {
signal.isGroupBlocked(id), return new Group(GroupId.unknownVersion(id),
0, (String) group.get("Name").getValue(),
false, (String) group.get("Description").getValue(),
signal.isMember(id)); 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 @Override

View file

@ -21,6 +21,12 @@ public class DbusProperty<T> {
this.setter = null; this.setter = null;
} }
public DbusProperty(final String name, final Consumer<T> setter) {
this.name = name;
this.getter = null;
this.setter = setter;
}
public String getName() { public String getName() {
return name; return name;
} }

View file

@ -1,7 +1,6 @@
package org.asamk.signal.dbus; package org.asamk.signal.dbus;
import org.asamk.Signal; import org.asamk.Signal;
import org.asamk.Signal.Error;
import org.asamk.signal.BaseConfig; import org.asamk.signal.BaseConfig;
import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.manager.AttachmentInvalidException; 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.Message;
import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.TypingAction; 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.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl; 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.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException; 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.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.freedesktop.dbus.types.Variant;
import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@ -40,6 +43,8 @@ import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -58,6 +63,7 @@ public class DbusSignalImpl implements Signal {
private DBusPath thisDevice; private DBusPath thisDevice;
private final List<StructDevice> devices = new ArrayList<>(); private final List<StructDevice> devices = new ArrayList<>();
private final List<StructGroup> groups = new ArrayList<>();
public DbusSignalImpl(final Manager m, DBusConnection connection, final String objectPath) { public DbusSignalImpl(final Manager m, DBusConnection connection, final String objectPath) {
this.m = m; this.m = m;
@ -67,6 +73,7 @@ public class DbusSignalImpl implements Signal {
public void initObjects() { public void initObjects() {
updateDevices(); updateDevices();
updateGroups();
} }
public void close() { public void close() {
@ -415,6 +422,22 @@ public class DbusSignalImpl implements Signal {
return ids; 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 @Override
public String getGroupName(final byte[] groupId) { public String getGroupName(final byte[] groupId) {
var group = m.getGroup(getGroupId(groupId)); var group = m.getGroup(getGroupId(groupId));
@ -431,10 +454,18 @@ public class DbusSignalImpl implements Signal {
if (group == null) { if (group == null) {
return List.of(); return List.of();
} else { } 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 @Override
public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) { public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) {
try { try {
@ -448,19 +479,11 @@ public class DbusSignalImpl implements Signal {
return results.first().serialize(); return results.first().serialize();
} else { } else {
final var results = m.updateGroup(getGroupId(groupId), final var results = m.updateGroup(getGroupId(groupId),
name, UpdateGroup.newBuilder()
null, .withName(name)
memberIdentifiers, .withMembers(memberIdentifiers)
null, .withAvatarFile(avatar == null ? null : new File(avatar))
null, .build());
null,
false,
null,
null,
null,
avatar == null ? null : new File(avatar),
null,
null);
if (results != null) { if (results != null) {
checkSendMessageResults(results.getTimestamp(), results.getResults()); checkSendMessageResults(results.getTimestamp(), results.getResults());
} }
@ -740,6 +763,10 @@ public class DbusSignalImpl implements Signal {
throw new Error.Failure(message.toString()); 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( private static Set<RecipientIdentifier.Single> getSingleRecipientIdentifiers(
final Collection<String> recipientStrings, final String localNumber final Collection<String> recipientStrings, final String localNumber
) throws DBusExecutionException { ) throws DBusExecutionException {
@ -817,6 +844,38 @@ public class DbusSignalImpl implements Signal {
this.devices.clear(); 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 { public class DbusSignalDeviceImpl extends DbusProperties implements Signal.Device {
private final org.asamk.signal.manager.api.Device 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());
}
}
}
} }