From 8e8eed7b061f1ed47cf9e30abb5e29ee08e3a3dc Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 13 May 2021 18:06:57 +0200 Subject: [PATCH 01/16] Update argparse4j --- build.gradle.kts | 2 +- src/main/java/org/asamk/signal/App.java | 9 ++++++--- .../java/org/asamk/signal/commands/DaemonCommand.java | 4 ++-- .../java/org/asamk/signal/commands/ReceiveCommand.java | 2 +- .../org/asamk/signal/commands/RemoteDeleteCommand.java | 2 +- src/main/java/org/asamk/signal/commands/SendCommand.java | 2 +- .../org/asamk/signal/commands/SendReactionCommand.java | 4 ++-- .../java/org/asamk/signal/commands/TrustCommand.java | 4 ++-- .../org/asamk/signal/commands/UnregisterCommand.java | 2 +- .../org/asamk/signal/commands/UpdateProfileCommand.java | 8 ++++---- 10 files changed, 21 insertions(+), 18 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3ad2c67b..70f2a2b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,7 +23,7 @@ repositories { dependencies { implementation("org.bouncycastle:bcprov-jdk15on:1.68") - implementation("net.sourceforge.argparse4j:argparse4j:0.8.1") + implementation("net.sourceforge.argparse4j:argparse4j:0.9.0") implementation("com.github.hypfvieh:dbus-java:3.3.0") implementation("org.slf4j:slf4j-simple:1.7.30") implementation(project(":lib")) diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index 521e453a..54c475b1 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -35,6 +35,8 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import static net.sourceforge.argparse4j.DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS; + public class App { private final static Logger logger = LoggerFactory.getLogger(App.class); @@ -42,7 +44,8 @@ public class App { private final Namespace ns; static ArgumentParser buildArgumentParser() { - var parser = ArgumentParsers.newFor("signal-cli") + var parser = ArgumentParsers.newFor("signal-cli", VERSION_0_9_0_DEFAULT_SETTINGS) + .includeArgumentNamesAsKeysInResult(true) .build() .defaultHelp(true) .description("Commandline interface for Signal.") @@ -101,7 +104,7 @@ public class App { var username = ns.getString("username"); final var useDbus = ns.getBoolean("dbus"); - final var useDbusSystem = ns.getBoolean("dbus_system"); + final var useDbusSystem = ns.getBoolean("dbus-system"); if (useDbus || useDbusSystem) { // If username is null, it will connect to the default object path initDbusClient(command, username, useDbusSystem); @@ -116,7 +119,7 @@ public class App { dataPath = getDefaultDataPath(); } - final var serviceEnvironmentCli = ns.get("service_environment"); + final var serviceEnvironmentCli = ns.get("service-environment"); final var serviceEnvironment = serviceEnvironmentCli == ServiceEnvironmentCli.LIVE ? ServiceEnvironment.LIVE : ServiceEnvironment.SANDBOX; diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 8d26e452..f54f3bf6 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -54,7 +54,7 @@ public class DaemonCommand implements MultiLocalCommand { logger.warn("\"--json\" option has been deprecated, please use the global \"--output=json\" instead."); } - boolean ignoreAttachments = ns.getBoolean("ignore_attachments"); + boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); DBusConnection.DBusBusType busType; if (ns.getBoolean("system")) { @@ -88,7 +88,7 @@ public class DaemonCommand implements MultiLocalCommand { logger.warn("\"--json\" option has been deprecated, please use the global \"--output=json\" instead."); } - boolean ignoreAttachments = ns.getBoolean("ignore_attachments"); + boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); DBusConnection.DBusBusType busType; if (ns.getBoolean("system")) { diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index f3f77347..47f9aa84 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -155,7 +155,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { returnOnTimeout = false; timeout = 3600; } - boolean ignoreAttachments = ns.getBoolean("ignore_attachments"); + boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); try { final var handler = inJson ? new JsonReceiveMessageHandler(m) : new ReceiveMessageHandler(m); m.receiveMessages((long) (timeout * 1000), diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java index 793b362a..8efd581b 100644 --- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java @@ -41,7 +41,7 @@ public class RemoteDeleteCommand implements DbusCommand { throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); } - final long targetTimestamp = ns.getLong("target_timestamp"); + final long targetTimestamp = ns.getLong("target-timestamp"); final var writer = new PlainTextWriterImpl(System.out); diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 3c4ccb77..312ec802 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -47,7 +47,7 @@ public class SendCommand implements DbusCommand { final List recipients = ns.getList("recipient"); final var isEndSession = ns.getBoolean("endsession"); final var groupIdString = ns.getString("group"); - final var isNoteToSelf = ns.getBoolean("note_to_self"); + final var isNoteToSelf = ns.getBoolean("note-to-self"); final var noRecipients = recipients == null || recipients.isEmpty(); if ((noRecipients && isEndSession) || (noRecipients && groupIdString == null && !isNoteToSelf)) { diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index f610e81b..44286acb 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -51,8 +51,8 @@ public class SendReactionCommand implements DbusCommand { final var emoji = ns.getString("emoji"); final boolean isRemove = ns.getBoolean("remove"); - final var targetAuthor = ns.getString("target_author"); - final long targetTimestamp = ns.getLong("target_timestamp"); + final var targetAuthor = ns.getString("target-author"); + final long targetTimestamp = ns.getLong("target-timestamp"); final var writer = new PlainTextWriterImpl(System.out); diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index 00371b59..d6debf86 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -28,7 +28,7 @@ public class TrustCommand implements LocalCommand { @Override public void handleCommand(final Namespace ns, final Manager m) throws CommandException { var number = ns.getString("number"); - if (ns.getBoolean("trust_all_known_keys")) { + if (ns.getBoolean("trust-all-known-keys")) { boolean res; try { res = m.trustIdentityAllKeys(number); @@ -39,7 +39,7 @@ public class TrustCommand implements LocalCommand { throw new UserErrorException("Failed to set the trust for this number, make sure the number is correct."); } } else { - var safetyNumber = ns.getString("verified_safety_number"); + var safetyNumber = ns.getString("verified-safety-number"); if (safetyNumber != null) { safetyNumber = safetyNumber.replaceAll(" ", ""); if (safetyNumber.length() == 66) { diff --git a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java index 72df842e..d6d27e28 100644 --- a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java @@ -23,7 +23,7 @@ public class UnregisterCommand implements LocalCommand { @Override public void handleCommand(final Namespace ns, final Manager m) throws CommandException { try { - if (ns.getBoolean("delete_account")) { + if (ns.getBoolean("delete-account")) { m.deleteAccount(); } else { m.unregister(); diff --git a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java index ce578f27..c2712adb 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java @@ -30,12 +30,12 @@ public class UpdateProfileCommand implements LocalCommand { @Override public void handleCommand(final Namespace ns, final Manager m) throws CommandException { - var givenName = ns.getString("given_name"); - var familyName = ns.getString("family_name"); + var givenName = ns.getString("given-name"); + var familyName = ns.getString("family-name"); var about = ns.getString("about"); - var aboutEmoji = ns.getString("about_emoji"); + var aboutEmoji = ns.getString("about-emoji"); var avatarPath = ns.getString("avatar"); - boolean removeAvatar = ns.getBoolean("remove_avatar"); + boolean removeAvatar = ns.getBoolean("remove-avatar"); Optional avatarFile = removeAvatar ? Optional.absent() -- 2.51.0 From dd0effc10c1c91675fdbc6c24937979c0e868752 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 13 May 2021 20:05:46 +0200 Subject: [PATCH 02/16] Add group descriptions --- .../org/asamk/signal/manager/Manager.java | 11 ++-- .../signal/manager/helper/GroupHelper.java | 6 +- .../manager/storage/groups/GroupInfo.java | 4 ++ .../manager/storage/groups/GroupInfoV2.java | 8 +++ .../asamk/signal/ReceiveMessageHandler.java | 24 +++++++- .../signal/commands/ListGroupsCommand.java | 7 ++- .../signal/commands/UpdateGroupCommand.java | 57 ++++++++++++++++++- .../org/asamk/signal/dbus/DbusSignalImpl.java | 1 + 8 files changed, 108 insertions(+), 10 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 096f1c07..39e6d8ce 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -783,17 +783,17 @@ public class Manager implements Closeable { } public Pair> updateGroup( - GroupId groupId, String name, List members, File avatarFile + GroupId groupId, String name, String description, List members, File avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { final var membersRecipientIds = members == null ? null : getSignalServiceAddresses(members); if (membersRecipientIds != null) { membersRecipientIds.remove(account.getSelfRecipientId()); } - return sendUpdateGroupMessage(groupId, name, membersRecipientIds, avatarFile); + return sendUpdateGroupMessage(groupId, name, description, membersRecipientIds, avatarFile); } private Pair> sendUpdateGroupMessage( - GroupId groupId, String name, Set members, File avatarFile + GroupId groupId, String name, String description, Set members, File avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { GroupInfo g; SignalServiceDataMessage.Builder messageBuilder; @@ -809,6 +809,7 @@ public class Manager implements Closeable { messageBuilder = getGroupUpdateMessageBuilder(gv1); g = gv1; } else { + // TODO set description as well final var gv2 = gv2Pair.first(); final var decryptedGroup = gv2Pair.second(); @@ -843,8 +844,8 @@ public class Manager implements Closeable { groupGroupChangePair.second()); } } - if (result == null || name != null || avatarFile != null) { - var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, name, avatarFile); + if (result == null || name != null || description != null || avatarFile != null) { + var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, name, description, avatarFile); if (avatarFile != null) { avatarStore.storeGroupAvatar(groupInfoV2.getGroupId(), outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 6abdff07..0bf4069e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -196,13 +196,17 @@ public class GroupHelper { } public Pair updateGroupV2( - GroupInfoV2 groupInfoV2, String name, File avatarFile + GroupInfoV2 groupInfoV2, String name, String description, File avatarFile ) throws IOException { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); var groupOperations = groupsV2Operations.forGroup(groupSecretParams); var change = name != null ? groupOperations.createModifyGroupTitle(name) : GroupChange.Actions.newBuilder(); + if (description != null) { + change.setModifyDescription(groupOperations.createModifyGroupDescription(description)); + } + if (avatarFile != null) { final var avatarBytes = readAvatarBytes(avatarFile); var avatarCdnKey = groupsV2Api.uploadAvatar(avatarBytes, diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java index 8f9146de..e2aaff4c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java @@ -14,6 +14,10 @@ public abstract class GroupInfo { public abstract String getTitle(); + public String getDescription() { + return null; + } + public abstract GroupInviteLinkUrl getGroupInviteLink(); public abstract Set getMembers(); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index b37e50a9..859c7209 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -59,6 +59,14 @@ public class GroupInfoV2 extends GroupInfo { return this.group.getTitle(); } + @Override + public String getDescription() { + if (this.group == null) { + return null; + } + return this.group.getDescription(); + } + @Override public GroupInviteLinkUrl getGroupInviteLink() { if (this.group == null || this.group.getInviteLinkPassword() == null || ( diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index e63ce548..b39fbf22 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -20,6 +20,7 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.InvalidNumberException; +import java.util.ArrayList; import java.util.Base64; import java.util.stream.Collectors; @@ -285,6 +286,14 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { DateUtils.formatTimestamp(rm.getTimestamp())); } } + if (syncMessage.getViewed().isPresent()) { + writer.println("Received sync viewed messages list"); + for (var vm : syncMessage.getViewed().get()) { + writer.println("- From: {} Message timestamp: {}", + formatContact(vm.getSender()), + DateUtils.formatTimestamp(vm.getTimestamp())); + } + } if (syncMessage.getRequest().isPresent()) { String type; if (syncMessage.getRequest().get().isContactsRequest()) { @@ -643,8 +652,19 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : ""); - writer.println("Voice note: {}", pointer.getVoiceNote() ? "yes" : "no"); - writer.println("Borderless: {}", pointer.isBorderless() ? "yes" : "no"); + final var flags = new ArrayList(); + if (pointer.getVoiceNote()) { + flags.add("voice note"); + } + if (pointer.isBorderless()) { + flags.add("borderless"); + } + if (pointer.isGif()) { + flags.add("video gif"); + } + if (flags.size() > 0) { + writer.println("Flags: {}", String.join(", ", flags)); + } if (pointer.getWidth() > 0 || pointer.getHeight() > 0) { writer.println("Dimensions: {}x{}", pointer.getWidth(), pointer.getHeight()); } diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index a3a55608..4886eac4 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -38,9 +38,10 @@ public class ListGroupsCommand implements LocalCommand { final var groupInviteLink = group.getGroupInviteLink(); writer.println( - "Id: {} Name: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Link: {}", + "Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Link: {}", group.getGroupId().toBase64(), group.getTitle(), + group.getDescription(), group.isMember(m.getSelfRecipientId()), group.isBlocked(), resolveMembers(m, group.getMembers()), @@ -81,6 +82,7 @@ public class ListGroupsCommand implements LocalCommand { jsonGroups.add(new JsonGroup(group.getGroupId().toBase64(), group.getTitle(), + group.getDescription(), group.isMember(m.getSelfRecipientId()), group.isBlocked(), resolveMembers(m, group.getMembers()), @@ -103,6 +105,7 @@ public class ListGroupsCommand implements LocalCommand { public String id; public String name; + public String description; public boolean isMember; public boolean isBlocked; @@ -114,6 +117,7 @@ public class ListGroupsCommand implements LocalCommand { public JsonGroup( String id, String name, + String description, boolean isMember, boolean isBlocked, Set members, @@ -123,6 +127,7 @@ public class ListGroupsCommand implements LocalCommand { ) { this.id = id; this.name = name; + this.description = description; this.isMember = isMember; this.isBlocked = isBlocked; diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 00730c82..48efde75 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -8,17 +8,26 @@ import org.asamk.signal.PlainTextWriterImpl; import org.asamk.signal.commands.exceptions.CommandException; 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.groups.GroupId; import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.util.InvalidNumberException; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Base64; import java.util.List; -public class UpdateGroupCommand implements DbusCommand { +public class UpdateGroupCommand implements DbusCommand, LocalCommand { private final static Logger logger = LoggerFactory.getLogger(UpdateGroupCommand.class); @@ -26,10 +35,56 @@ public class UpdateGroupCommand implements DbusCommand { public void attachToSubparser(final Subparser subparser) { subparser.addArgument("-g", "--group").help("Specify the recipient group ID."); subparser.addArgument("-n", "--name").help("Specify the new group name."); + subparser.addArgument("-d", "--description").help("Specify the new group description."); subparser.addArgument("-a", "--avatar").help("Specify a new group avatar image file"); subparser.addArgument("-m", "--member").nargs("*").help("Specify one or more members to add to the group"); } + @Override + public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + final var writer = new PlainTextWriterImpl(System.out); + GroupId groupId = null; + final var groupIdString = ns.getString("group"); + if (groupIdString != null) { + try { + groupId = Util.decodeGroupId(groupIdString); + } catch (GroupIdFormatException e) { + throw new UserErrorException("Invalid group id:" + e.getMessage()); + } + } + + var groupName = ns.getString("name"); + + var groupDescription = ns.getString("description"); + + List groupMembers = ns.getList("member"); + + var groupAvatar = ns.getString("avatar"); + + try { + var results = m.updateGroup(groupId, + groupName, + groupDescription, + groupMembers, + groupAvatar == null ? null : new File(groupAvatar)); + ErrorUtils.handleTimestampAndSendMessageResults(writer, 0, results.second()); + final var newGroupId = results.first(); + if (groupId == null) { + writer.println("Created new group: \"{}\"", newGroupId.toBase64()); + } + } catch (AttachmentInvalidException e) { + throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage()); + } catch (GroupNotFoundException e) { + logger.warn("Unknown group id: {}", groupIdString); + } catch (NotAGroupMemberException e) { + logger.warn("You're not a group member"); + } catch (InvalidNumberException e) { + throw new UserErrorException("Failed to parse member number: " + e.getMessage()); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + } + } + @Override public void handleCommand(final Namespace ns, final Signal signal) throws CommandException { final var writer = new PlainTextWriterImpl(System.out); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 325ae7a1..a8ccf3d0 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -337,6 +337,7 @@ public class DbusSignalImpl implements Signal { } final var results = m.updateGroup(groupId == null ? null : GroupId.unknownVersion(groupId), name, + null, members, avatar == null ? null : new File(avatar)); checkSendMessageResults(0, results.second()); -- 2.51.0 From 4ebacd0e1ff089ab961cbc557324801df46f33df Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 14 May 2021 21:21:09 +0200 Subject: [PATCH 03/16] Split createGroup out of updateGroup method --- .../org/asamk/signal/manager/Manager.java | 200 ++++++++++-------- .../signal/commands/UpdateGroupCommand.java | 19 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 20 +- 3 files changed, 132 insertions(+), 107 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 39e6d8ce..45404154 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -782,96 +782,83 @@ public class Manager implements Closeable { return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId())); } - public Pair> updateGroup( + public Pair> createGroup( + String name, List members, File avatarFile + ) throws IOException, AttachmentInvalidException, InvalidNumberException { + return createGroup(name, members == null ? null : getSignalServiceAddresses(members), avatarFile); + } + + private Pair> createGroup( + String name, Set members, File avatarFile + ) throws IOException, AttachmentInvalidException { + final var selfRecipientId = account.getSelfRecipientId(); + if (members != null && members.contains(selfRecipientId)) { + members = new HashSet<>(members); + members.remove(selfRecipientId); + } + + var gv2Pair = groupHelper.createGroupV2(name == null ? "" : name, + members == null ? Set.of() : members, + avatarFile); + + SignalServiceDataMessage.Builder messageBuilder; + if (gv2Pair == null) { + // Failed to create v2 group, creating v1 group instead + var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); + gv1.addMembers(List.of(selfRecipientId)); + final var result = updateGroupV1(gv1, name, members, avatarFile); + return new Pair<>(gv1.getGroupId(), result.second()); + } + + final var gv2 = gv2Pair.first(); + final var decryptedGroup = gv2Pair.second(); + + gv2.setGroup(decryptedGroup, this::resolveRecipient); + if (avatarFile != null) { + avatarStore.storeGroupAvatar(gv2.getGroupId(), + outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); + } + messageBuilder = getGroupUpdateMessageBuilder(gv2, null); + account.getGroupStore().updateGroup(gv2); + + final var result = sendMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId)); + return new Pair<>(gv2.getGroupId(), result.second()); + } + + public Pair> updateGroup( GroupId groupId, String name, String description, List members, File avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { - final var membersRecipientIds = members == null ? null : getSignalServiceAddresses(members); - if (membersRecipientIds != null) { - membersRecipientIds.remove(account.getSelfRecipientId()); - } - return sendUpdateGroupMessage(groupId, name, description, membersRecipientIds, avatarFile); + return updateGroup(groupId, + name, + description, + members == null ? null : getSignalServiceAddresses(members), + avatarFile); } - private Pair> sendUpdateGroupMessage( + private Pair> updateGroup( GroupId groupId, String name, String description, Set members, File avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - GroupInfo g; - SignalServiceDataMessage.Builder messageBuilder; - if (groupId == null) { - // Create new group - var gv2Pair = groupHelper.createGroupV2(name == null ? "" : name, - members == null ? Set.of() : members, - avatarFile); - if (gv2Pair == null) { - var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); - gv1.addMembers(List.of(account.getSelfRecipientId())); - updateGroupV1(gv1, name, members, avatarFile); - messageBuilder = getGroupUpdateMessageBuilder(gv1); - g = gv1; - } else { - // TODO set description as well - final var gv2 = gv2Pair.first(); - final var decryptedGroup = gv2Pair.second(); - - gv2.setGroup(decryptedGroup, this::resolveRecipient); - if (avatarFile != null) { - avatarStore.storeGroupAvatar(gv2.getGroupId(), - outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); - } - messageBuilder = getGroupUpdateMessageBuilder(gv2, null); - g = gv2; - } - } else { - var group = getGroupForUpdating(groupId); - if (group instanceof GroupInfoV2) { - final var groupInfoV2 = (GroupInfoV2) group; - - Pair> result = null; - if (groupInfoV2.isPendingMember(account.getSelfRecipientId())) { - var groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2); - result = sendUpdateGroupMessage(groupInfoV2, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } + var group = getGroupForUpdating(groupId); - if (members != null) { - final var newMembers = new HashSet<>(members); - newMembers.removeAll(group.getMembers()); - if (newMembers.size() > 0) { - var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers); - result = sendUpdateGroupMessage(groupInfoV2, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } - } - if (result == null || name != null || description != null || avatarFile != null) { - var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, name, description, avatarFile); - if (avatarFile != null) { - avatarStore.storeGroupAvatar(groupInfoV2.getGroupId(), - outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); - } - result = sendUpdateGroupMessage(groupInfoV2, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } - - return new Pair<>(group.getGroupId(), result.second()); - } else { - var gv1 = (GroupInfoV1) group; - updateGroupV1(gv1, name, members, avatarFile); - messageBuilder = getGroupUpdateMessageBuilder(gv1); - g = gv1; - } + if (group instanceof GroupInfoV2) { + return updateGroupV2((GroupInfoV2) group, name, description, members, avatarFile); } - account.getGroupStore().updateGroup(g); + return updateGroupV1((GroupInfoV1) group, name, members, avatarFile); + } + + private Pair> updateGroupV1( + final GroupInfoV1 gv1, final String name, final Set members, final File avatarFile + ) throws IOException, AttachmentInvalidException { + updateGroupV1Details(gv1, name, members, avatarFile); + var messageBuilder = getGroupUpdateMessageBuilder(gv1); + + account.getGroupStore().updateGroup(gv1); - final var result = sendMessage(messageBuilder, - g.getMembersIncludingPendingWithout(account.getSelfRecipientId())); - return new Pair<>(g.getGroupId(), result.second()); + return sendMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } - private void updateGroupV1( + private void updateGroupV1Details( final GroupInfoV1 g, final String name, final Collection members, final File avatarFile ) throws IOException { if (name != null) { @@ -909,13 +896,40 @@ public class Manager implements Closeable { } } - public Pair> joinGroup( - GroupInviteLinkUrl inviteLinkUrl - ) throws IOException, GroupLinkNotActiveException { - return sendJoinGroupMessage(inviteLinkUrl); + private Pair> updateGroupV2( + final GroupInfoV2 group, + final String name, + final String description, + final Set members, + final File avatarFile + ) throws IOException { + Pair> result = null; + if (group.isPendingMember(account.getSelfRecipientId())) { + var groupGroupChangePair = groupHelper.acceptInvite(group); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (members != null) { + final var newMembers = new HashSet<>(members); + newMembers.removeAll(group.getMembers()); + if (newMembers.size() > 0) { + var groupGroupChangePair = groupHelper.updateGroupV2(group, newMembers); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + } + if (result == null || name != null || description != null || avatarFile != null) { + var groupGroupChangePair = groupHelper.updateGroupV2(group, name, description, avatarFile); + if (avatarFile != null) { + avatarStore.storeGroupAvatar(group.getGroupId(), + outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); + } + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + return result; } - private Pair> sendJoinGroupMessage( + public Pair> joinGroup( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, GroupLinkNotActiveException { final var groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), @@ -932,11 +946,20 @@ public class Manager implements Closeable { return new Pair<>(group.getGroupId(), List.of()); } - final var result = sendUpdateGroupMessage(group, group.getGroup(), groupChange); + final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange); return new Pair<>(group.getGroupId(), result.second()); } + private Pair> sendUpdateGroupV2Message( + GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange + ) throws IOException { + group.setGroup(newDecryptedGroup, this::resolveRecipient); + final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); + account.getGroupStore().updateGroup(group); + return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + } + private static int currentTimeDays() { return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()); } @@ -959,15 +982,6 @@ public class Manager implements Closeable { } } - private Pair> sendUpdateGroupMessage( - GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange - ) throws IOException { - group.setGroup(newDecryptedGroup, this::resolveRecipient); - final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); - account.getGroupStore().updateGroup(group); - return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfRecipientId())); - } - Pair> sendGroupInfoMessage( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 48efde75..00dd10d9 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -62,15 +62,20 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { var groupAvatar = ns.getString("avatar"); try { - var results = m.updateGroup(groupId, - groupName, - groupDescription, - groupMembers, - groupAvatar == null ? null : new File(groupAvatar)); - ErrorUtils.handleTimestampAndSendMessageResults(writer, 0, results.second()); - final var newGroupId = results.first(); if (groupId == null) { + var results = m.createGroup(groupName, + groupMembers, + groupAvatar == null ? null : new File(groupAvatar)); + ErrorUtils.handleTimestampAndSendMessageResults(writer, 0, results.second()); + final var newGroupId = results.first(); writer.println("Created new group: \"{}\"", newGroupId.toBase64()); + } else { + var results = m.updateGroup(groupId, + groupName, + groupDescription, + groupMembers, + groupAvatar == null ? null : new File(groupAvatar)); + ErrorUtils.handleTimestampAndSendMessageResults(writer, results.first(), results.second()); } } catch (AttachmentInvalidException e) { throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage()); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index a8ccf3d0..cdc9e8d5 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -335,13 +335,19 @@ public class DbusSignalImpl implements Signal { if (avatar.isEmpty()) { avatar = null; } - final var results = m.updateGroup(groupId == null ? null : GroupId.unknownVersion(groupId), - name, - null, - members, - avatar == null ? null : new File(avatar)); - checkSendMessageResults(0, results.second()); - return results.first().serialize(); + if (groupId == null) { + final var results = m.createGroup(name, members, avatar == null ? null : new File(avatar)); + checkSendMessageResults(0, results.second()); + return results.first().serialize(); + } else { + final var results = m.updateGroup(GroupId.unknownVersion(groupId), + name, + null, + members, + avatar == null ? null : new File(avatar)); + checkSendMessageResults(results.first(), results.second()); + return groupId; + } } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { -- 2.51.0 From a91e3f762e0e5d6a9cf9e208fe85207d7829e22d Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 14 May 2021 22:23:13 +0200 Subject: [PATCH 04/16] Implement remove group members --- .../org/asamk/signal/manager/Manager.java | 42 +++++++++++++++++-- .../signal/manager/helper/GroupHelper.java | 34 ++++++++++++++- .../signal/commands/UpdateGroupCommand.java | 6 +++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 1 + 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 45404154..9d73ac81 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -826,22 +826,33 @@ public class Manager implements Closeable { } public Pair> updateGroup( - GroupId groupId, String name, String description, List members, File avatarFile + GroupId groupId, + String name, + String description, + List members, + List removeMembers, + File avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { return updateGroup(groupId, name, description, members == null ? null : getSignalServiceAddresses(members), + removeMembers == null ? null : getSignalServiceAddresses(removeMembers), avatarFile); } private Pair> updateGroup( - GroupId groupId, String name, String description, Set members, File avatarFile + GroupId groupId, + String name, + String description, + Set members, + final Set removeMembers, + File avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { var group = getGroupForUpdating(groupId); if (group instanceof GroupInfoV2) { - return updateGroupV2((GroupInfoV2) group, name, description, members, avatarFile); + return updateGroupV2((GroupInfoV2) group, name, description, members, removeMembers, avatarFile); } return updateGroupV1((GroupInfoV1) group, name, members, avatarFile); @@ -901,6 +912,7 @@ public class Manager implements Closeable { final String name, final String description, final Set members, + final Set removeMembers, final File avatarFile ) throws IOException { Pair> result = null; @@ -917,6 +929,24 @@ public class Manager implements Closeable { result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); } } + + if (removeMembers != null) { + var existingRemoveMembers = new HashSet<>(removeMembers); + existingRemoveMembers.retainAll(group.getMembers()); + existingRemoveMembers.remove(getSelfRecipientId());// self can be removed with sendQuitGroupMessage + if (existingRemoveMembers.size() > 0) { + var groupGroupChangePair = groupHelper.removeMembers(group, existingRemoveMembers); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + var pendingRemoveMembers = new HashSet<>(removeMembers); + pendingRemoveMembers.retainAll(group.getPendingMembers()); + if (pendingRemoveMembers.size() > 0) { + var groupGroupChangePair = groupHelper.revokeInvitedMembers(group, pendingRemoveMembers); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + } + if (result == null || name != null || description != null || avatarFile != null) { var groupGroupChangePair = groupHelper.updateGroupV2(group, name, description, avatarFile); if (avatarFile != null) { @@ -954,10 +984,14 @@ public class Manager implements Closeable { private Pair> sendUpdateGroupV2Message( GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange ) throws IOException { + final var selfRecipientId = account.getSelfRecipientId(); + final var members = group.getMembersIncludingPendingWithout(selfRecipientId); group.setGroup(newDecryptedGroup, this::resolveRecipient); + members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId)); + final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); account.getGroupStore().updateGroup(group); - return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + return sendMessage(messageBuilder, members); } private static int currentTimeDays() { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 0bf4069e..527fa7ab 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -263,6 +263,34 @@ public class GroupHelper { } } + public Pair removeMembers( + GroupInfoV2 groupInfoV2, Set members + ) throws IOException { + final var memberUuids = members.stream() + .map(addressResolver::resolveSignalServiceAddress) + .map(SignalServiceAddress::getUuid) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + return ejectMembers(groupInfoV2, memberUuids); + } + + public Pair revokeInvitedMembers( + GroupInfoV2 groupInfoV2, Set members + ) throws IOException { + var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList(); + final var memberUuids = members.stream() + .map(addressResolver::resolveSignalServiceAddress) + .map(SignalServiceAddress::getUuid) + .filter(Optional::isPresent) + .map(Optional::get) + .map(uuid -> DecryptedGroupUtil.findPendingByUuid(pendingMembersList, uuid)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + return revokeInvites(groupInfoV2, memberUuids); + } + public GroupChange joinGroup( GroupMasterKey groupMasterKey, GroupLinkPassword groupLinkPassword, @@ -309,7 +337,7 @@ public class GroupHelper { return commitChange(groupInfoV2, change); } - public Pair revokeInvites( + private Pair revokeInvites( GroupInfoV2 groupInfoV2, Set pendingMembers ) throws IOException { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); @@ -324,7 +352,9 @@ public class GroupHelper { return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts)); } - public Pair ejectMembers(GroupInfoV2 groupInfoV2, Set uuids) throws IOException { + private Pair ejectMembers( + GroupInfoV2 groupInfoV2, Set uuids + ) throws IOException { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); final var groupOperations = groupsV2Operations.forGroup(groupSecretParams); return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(uuids)); diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 00dd10d9..513ec2e4 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -38,6 +38,9 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { subparser.addArgument("-d", "--description").help("Specify the new group description."); subparser.addArgument("-a", "--avatar").help("Specify a new group avatar image file"); subparser.addArgument("-m", "--member").nargs("*").help("Specify one or more members to add to the group"); + subparser.addArgument("-r", "--remove-member") + .nargs("*") + .help("Specify one or more members to remove from the group"); } @Override @@ -59,6 +62,8 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { List groupMembers = ns.getList("member"); + List groupRemoveMembers = ns.getList("remove-member"); + var groupAvatar = ns.getString("avatar"); try { @@ -74,6 +79,7 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { groupName, groupDescription, groupMembers, + groupRemoveMembers, groupAvatar == null ? null : new File(groupAvatar)); ErrorUtils.handleTimestampAndSendMessageResults(writer, results.first(), results.second()); } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index cdc9e8d5..6606e966 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -344,6 +344,7 @@ public class DbusSignalImpl implements Signal { name, null, members, + null, avatar == null ? null : new File(avatar)); checkSendMessageResults(results.first(), results.second()); return groupId; -- 2.51.0 From b972522d749d6f82898c7919f2f5b36a7dbc2f9b Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 15 May 2021 10:15:16 +0200 Subject: [PATCH 05/16] Rename group v2 helper --- .../org/asamk/signal/manager/Manager.java | 32 ++++++++++--------- .../{GroupHelper.java => GroupV2Helper.java} | 16 +++++----- 2 files changed, 25 insertions(+), 23 deletions(-) rename lib/src/main/java/org/asamk/signal/manager/helper/{GroupHelper.java => GroupV2Helper.java} (98%) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 9d73ac81..cd0ce561 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -26,7 +26,7 @@ import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.manager.helper.GroupHelper; +import org.asamk.signal.manager.helper.GroupV2Helper; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; @@ -191,7 +191,7 @@ public class Manager implements Closeable { private final UnidentifiedAccessHelper unidentifiedAccessHelper; private final ProfileHelper profileHelper; - private final GroupHelper groupHelper; + private final GroupV2Helper groupV2Helper; private final PinHelper pinHelper; private final AvatarStore avatarStore; private final AttachmentStore attachmentStore; @@ -259,7 +259,7 @@ public class Manager implements Closeable { unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(), () -> messageReceiver, this::resolveSignalServiceAddress); - this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential, + this.groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential, this::getRecipientProfile, account::getSelfRecipientId, groupsV2Operations, @@ -773,7 +773,7 @@ public class Manager implements Closeable { account.getGroupStore().updateGroup(groupInfoV1); } else { final var groupInfoV2 = (GroupInfoV2) g; - final var groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2); + final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2); groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient); messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); account.getGroupStore().updateGroup(groupInfoV2); @@ -797,7 +797,7 @@ public class Manager implements Closeable { members.remove(selfRecipientId); } - var gv2Pair = groupHelper.createGroupV2(name == null ? "" : name, + var gv2Pair = groupV2Helper.createGroup(name == null ? "" : name, members == null ? Set.of() : members, avatarFile); @@ -917,7 +917,7 @@ public class Manager implements Closeable { ) throws IOException { Pair> result = null; if (group.isPendingMember(account.getSelfRecipientId())) { - var groupGroupChangePair = groupHelper.acceptInvite(group); + var groupGroupChangePair = groupV2Helper.acceptInvite(group); result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); } @@ -925,7 +925,7 @@ public class Manager implements Closeable { final var newMembers = new HashSet<>(members); newMembers.removeAll(group.getMembers()); if (newMembers.size() > 0) { - var groupGroupChangePair = groupHelper.updateGroupV2(group, newMembers); + var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers); result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); } } @@ -935,20 +935,20 @@ public class Manager implements Closeable { existingRemoveMembers.retainAll(group.getMembers()); existingRemoveMembers.remove(getSelfRecipientId());// self can be removed with sendQuitGroupMessage if (existingRemoveMembers.size() > 0) { - var groupGroupChangePair = groupHelper.removeMembers(group, existingRemoveMembers); + var groupGroupChangePair = groupV2Helper.removeMembers(group, existingRemoveMembers); result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); } var pendingRemoveMembers = new HashSet<>(removeMembers); pendingRemoveMembers.retainAll(group.getPendingMembers()); if (pendingRemoveMembers.size() > 0) { - var groupGroupChangePair = groupHelper.revokeInvitedMembers(group, pendingRemoveMembers); + var groupGroupChangePair = groupV2Helper.revokeInvitedMembers(group, pendingRemoveMembers); result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); } } if (result == null || name != null || description != null || avatarFile != null) { - var groupGroupChangePair = groupHelper.updateGroupV2(group, name, description, avatarFile); + var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile); if (avatarFile != null) { avatarStore.storeGroupAvatar(group.getGroupId(), outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); @@ -962,9 +962,9 @@ public class Manager implements Closeable { public Pair> joinGroup( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, GroupLinkNotActiveException { - final var groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), + final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), inviteLinkUrl.getPassword()); - final var groupChange = groupHelper.joinGroup(inviteLinkUrl.getGroupMasterKey(), + final var groupChange = groupV2Helper.joinGroup(inviteLinkUrl.getGroupMasterKey(), inviteLinkUrl.getPassword(), groupJoinInfo); final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(), @@ -1762,10 +1762,12 @@ public class Manager implements Closeable { if (signedGroupChange != null && groupInfoV2.getGroup() != null && groupInfoV2.getGroup().getRevision() + 1 == revision) { - group = groupHelper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), signedGroupChange, groupMasterKey); + group = groupV2Helper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), + signedGroupChange, + groupMasterKey); } if (group == null) { - group = groupHelper.getDecryptedGroup(groupSecretParams); + group = groupV2Helper.getDecryptedGroup(groupSecretParams); } if (group != null) { storeProfileKeysFromMembers(group); @@ -2577,7 +2579,7 @@ public class Manager implements Closeable { final var group = account.getGroupStore().getGroup(groupId); if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey()); - ((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams), this::resolveRecipient); + ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), this::resolveRecipient); account.getGroupStore().updateGroup(group); } return group; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java similarity index 98% rename from lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java rename to lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 527fa7ab..94df2a41 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -43,9 +43,9 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; -public class GroupHelper { +public class GroupV2Helper { - private final static Logger logger = LoggerFactory.getLogger(GroupHelper.class); + private final static Logger logger = LoggerFactory.getLogger(GroupV2Helper.class); private final ProfileKeyCredentialProvider profileKeyCredentialProvider; @@ -61,7 +61,7 @@ public class GroupHelper { private final SignalServiceAddressResolver addressResolver; - public GroupHelper( + public GroupV2Helper( final ProfileKeyCredentialProvider profileKeyCredentialProvider, final ProfileProvider profileProvider, final SelfRecipientIdProvider selfRecipientIdProvider, @@ -100,11 +100,11 @@ public class GroupHelper { groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams)); } - public Pair createGroupV2( + public Pair createGroup( String name, Set members, File avatarFile ) throws IOException { final var avatarBytes = readAvatarBytes(avatarFile); - final var newGroup = buildNewGroupV2(name, members, avatarBytes); + final var newGroup = buildNewGroup(name, members, avatarBytes); if (newGroup == null) { return null; } @@ -141,7 +141,7 @@ public class GroupHelper { return avatarBytes; } - private GroupsV2Operations.NewGroup buildNewGroupV2( + private GroupsV2Operations.NewGroup buildNewGroup( String name, Set members, byte[] avatar ) { final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientIdProvider.getSelfRecipientId()); @@ -195,7 +195,7 @@ public class GroupHelper { return true; } - public Pair updateGroupV2( + public Pair updateGroup( GroupInfoV2 groupInfoV2, String name, String description, File avatarFile ) throws IOException { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); @@ -224,7 +224,7 @@ public class GroupHelper { return commitChange(groupInfoV2, change); } - public Pair updateGroupV2( + public Pair addMembers( GroupInfoV2 groupInfoV2, Set newMembers ) throws IOException { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); -- 2.51.0 From 3de30e166f4eb6be0cd10548ed2c6cce2fe2282e Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 15 May 2021 11:08:01 +0200 Subject: [PATCH 06/16] Implement add/remove admin privileges --- .../org/asamk/signal/manager/Manager.java | 54 ++++++++++++++++--- .../signal/manager/helper/GroupV2Helper.java | 27 +++++++--- .../manager/storage/groups/GroupInfo.java | 4 ++ .../manager/storage/groups/GroupInfoV2.java | 14 +++++ .../signal/commands/ListGroupsCommand.java | 7 ++- .../signal/commands/UpdateGroupCommand.java | 11 ++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 2 + 7 files changed, 104 insertions(+), 15 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index cd0ce561..b003279a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -831,6 +831,8 @@ public class Manager implements Closeable { String description, List members, List removeMembers, + List admins, + List removeAdmins, File avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { return updateGroup(groupId, @@ -838,21 +840,32 @@ public class Manager implements Closeable { description, members == null ? null : getSignalServiceAddresses(members), removeMembers == null ? null : getSignalServiceAddresses(removeMembers), + admins == null ? null : getSignalServiceAddresses(admins), + removeAdmins == null ? null : getSignalServiceAddresses(removeAdmins), avatarFile); } private Pair> updateGroup( - GroupId groupId, - String name, - String description, - Set members, + final GroupId groupId, + final String name, + final String description, + final Set members, final Set removeMembers, - File avatarFile + final Set admins, + final Set removeAdmins, + final File avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { var group = getGroupForUpdating(groupId); if (group instanceof GroupInfoV2) { - return updateGroupV2((GroupInfoV2) group, name, description, members, removeMembers, avatarFile); + return updateGroupV2((GroupInfoV2) group, + name, + description, + members, + removeMembers, + admins, + removeAdmins, + avatarFile); } return updateGroupV1((GroupInfoV1) group, name, members, avatarFile); @@ -913,6 +926,8 @@ public class Manager implements Closeable { final String description, final Set members, final Set removeMembers, + final Set admins, + final Set removeAdmins, final File avatarFile ) throws IOException { Pair> result = null; @@ -947,6 +962,33 @@ public class Manager implements Closeable { } } + if (admins != null) { + final var newAdmins = new HashSet<>(admins); + newAdmins.retainAll(group.getMembers()); + newAdmins.removeAll(group.getAdminMembers()); + if (newAdmins.size() > 0) { + for (var admin : newAdmins) { + var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true); + result = sendUpdateGroupV2Message(group, + groupGroupChangePair.first(), + groupGroupChangePair.second()); + } + } + } + + if (removeAdmins != null) { + final var existingRemoveAdmins = new HashSet<>(removeAdmins); + existingRemoveAdmins.retainAll(group.getAdminMembers()); + if (existingRemoveAdmins.size() > 0) { + for (var admin : existingRemoveAdmins) { + var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false); + result = sendUpdateGroupV2Message(group, + groupGroupChangePair.first(), + groupGroupChangePair.second()); + } + } + } + if (result == null || name != null || description != null || avatarFile != null) { var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile); if (avatarFile != null) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 94df2a41..6dab6dab 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -227,8 +227,7 @@ public class GroupV2Helper { public Pair addMembers( GroupInfoV2 groupInfoV2, Set newMembers ) throws IOException { - final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); - var groupOperations = groupsV2Operations.forGroup(groupSecretParams); + GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); if (!areMembersValid(newMembers)) { throw new IOException("Failed to update group"); @@ -318,8 +317,7 @@ public class GroupV2Helper { } public Pair acceptInvite(GroupInfoV2 groupInfoV2) throws IOException { - final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); - final var groupOperations = groupsV2Operations.forGroup(groupSecretParams); + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId(); final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId); @@ -337,11 +335,25 @@ public class GroupV2Helper { return commitChange(groupInfoV2, change); } + public Pair setMemberAdmin( + GroupInfoV2 groupInfoV2, RecipientId recipientId, boolean admin + ) throws IOException { + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + final var newRole = admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT; + final var change = groupOperations.createChangeMemberRole(address.getUuid().get(), newRole); + return commitChange(groupInfoV2, change); + } + + private GroupsV2Operations.GroupOperations getGroupOperations(final GroupInfoV2 groupInfoV2) { + final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); + return groupsV2Operations.forGroup(groupSecretParams); + } + private Pair revokeInvites( GroupInfoV2 groupInfoV2, Set pendingMembers ) throws IOException { - final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); - final var groupOperations = groupsV2Operations.forGroup(groupSecretParams); + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final var uuidCipherTexts = pendingMembers.stream().map(member -> { try { return new UuidCiphertext(member.getUuidCipherText().toByteArray()); @@ -355,8 +367,7 @@ public class GroupV2Helper { private Pair ejectMembers( GroupInfoV2 groupInfoV2, Set uuids ) throws IOException { - final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); - final var groupOperations = groupsV2Operations.forGroup(groupSecretParams); + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(uuids)); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java index e2aaff4c..211e0f96 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java @@ -30,6 +30,10 @@ public abstract class GroupInfo { return Set.of(); } + public Set getAdminMembers() { + return Set.of(); + } + public abstract boolean isBlocked(); public abstract void setBlocked(boolean blocked); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index 859c7209..308220cb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -5,6 +5,7 @@ import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.signal.storageservice.protos.groups.AccessControl; +import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.zkgroup.groups.GroupMasterKey; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -116,6 +117,19 @@ public class GroupInfoV2 extends GroupInfo { .collect(Collectors.toSet()); } + @Override + public Set getAdminMembers() { + if (this.group == null) { + return Set.of(); + } + return group.getMembersList() + .stream() + .filter(m -> m.getRole() == Member.Role.ADMINISTRATOR) + .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null)) + .map(recipientResolver::resolveRecipient) + .collect(Collectors.toSet()); + } + @Override public boolean isBlocked() { return blocked; diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index 4886eac4..aab91077 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -38,7 +38,7 @@ public class ListGroupsCommand implements LocalCommand { final var groupInviteLink = group.getGroupInviteLink(); writer.println( - "Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Link: {}", + "Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Admins: {} Link: {}", group.getGroupId().toBase64(), group.getTitle(), group.getDescription(), @@ -47,6 +47,7 @@ public class ListGroupsCommand implements LocalCommand { resolveMembers(m, group.getMembers()), resolveMembers(m, group.getPendingMembers()), resolveMembers(m, group.getRequestingMembers()), + resolveMembers(m, group.getAdminMembers()), groupInviteLink == null ? '-' : groupInviteLink.getUrl()); } else { writer.println("Id: {} Name: {} Active: {} Blocked: {}", @@ -88,6 +89,7 @@ public class ListGroupsCommand implements LocalCommand { resolveMembers(m, group.getMembers()), resolveMembers(m, group.getPendingMembers()), resolveMembers(m, group.getRequestingMembers()), + resolveMembers(m, group.getAdminMembers()), groupInviteLink == null ? null : groupInviteLink.getUrl())); } @@ -112,6 +114,7 @@ public class ListGroupsCommand implements LocalCommand { public Set members; public Set pendingMembers; public Set requestingMembers; + public Set admins; public String groupInviteLink; public JsonGroup( @@ -123,6 +126,7 @@ public class ListGroupsCommand implements LocalCommand { Set members, Set pendingMembers, Set requestingMembers, + Set admins, String groupInviteLink ) { this.id = id; @@ -134,6 +138,7 @@ public class ListGroupsCommand implements LocalCommand { this.members = members; this.pendingMembers = pendingMembers; this.requestingMembers = requestingMembers; + this.admins = admins; this.groupInviteLink = groupInviteLink; } } diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 513ec2e4..87307446 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -41,6 +41,11 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { subparser.addArgument("-r", "--remove-member") .nargs("*") .help("Specify one or more members to remove from the group"); + subparser.addArgument("--admin").nargs("*").help("Specify one or more members to make a group admin"); + subparser.addArgument("--remove-admin") + .nargs("*") + .help("Specify one or more members to remove group admin privileges"); + } @Override @@ -64,6 +69,10 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { List groupRemoveMembers = ns.getList("remove-member"); + List groupAdmins = ns.getList("admin"); + + List groupRemoveAdmins = ns.getList("remove-admin"); + var groupAvatar = ns.getString("avatar"); try { @@ -80,6 +89,8 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { groupDescription, groupMembers, groupRemoveMembers, + groupAdmins, + groupRemoveAdmins, groupAvatar == null ? null : new File(groupAvatar)); ErrorUtils.handleTimestampAndSendMessageResults(writer, results.first(), results.second()); } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 6606e966..9b83ace5 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -345,6 +345,8 @@ public class DbusSignalImpl implements Signal { null, members, null, + null, + null, avatar == null ? null : new File(avatar)); checkSendMessageResults(results.first(), results.second()); return groupId; -- 2.51.0 From 03589f858ba2cf52df4d85a9d68df3f3cda5cb74 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 15 May 2021 13:14:12 +0200 Subject: [PATCH 07/16] Implement configuring of group link --- .../org/asamk/signal/manager/Manager.java | 21 +++++++++++ .../signal/manager/groups/GroupLinkState.java | 7 ++++ .../signal/manager/helper/GroupV2Helper.java | 37 +++++++++++++++++++ .../manager/storage/groups/GroupInfoV2.java | 2 +- .../java/org/asamk/signal/GroupLinkState.java | 35 ++++++++++++++++++ .../signal/commands/UpdateGroupCommand.java | 22 +++++++++-- .../org/asamk/signal/dbus/DbusSignalImpl.java | 2 + 7 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/groups/GroupLinkState.java create mode 100644 src/main/java/org/asamk/signal/GroupLinkState.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index b003279a..03f4e8e9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -23,6 +23,7 @@ import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupIdV1; 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.GroupUtils; import org.asamk.signal.manager.groups.NotAGroupMemberException; @@ -833,6 +834,8 @@ public class Manager implements Closeable { List removeMembers, List admins, List removeAdmins, + boolean resetGroupLink, + GroupLinkState groupLinkState, File avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { return updateGroup(groupId, @@ -842,6 +845,8 @@ public class Manager implements Closeable { removeMembers == null ? null : getSignalServiceAddresses(removeMembers), admins == null ? null : getSignalServiceAddresses(admins), removeAdmins == null ? null : getSignalServiceAddresses(removeAdmins), + resetGroupLink, + groupLinkState, avatarFile); } @@ -853,6 +858,8 @@ public class Manager implements Closeable { final Set removeMembers, final Set admins, final Set removeAdmins, + final boolean resetGroupLink, + final GroupLinkState groupLinkState, final File avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { var group = getGroupForUpdating(groupId); @@ -865,6 +872,8 @@ public class Manager implements Closeable { removeMembers, admins, removeAdmins, + resetGroupLink, + groupLinkState, avatarFile); } @@ -928,6 +937,8 @@ public class Manager implements Closeable { final Set removeMembers, final Set admins, final Set removeAdmins, + final boolean resetGroupLink, + final GroupLinkState groupLinkState, final File avatarFile ) throws IOException { Pair> result = null; @@ -989,6 +1000,16 @@ public class Manager implements Closeable { } } + if (resetGroupLink) { + var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (groupLinkState != null) { + var groupGroupChangePair = groupV2Helper.setGroupLinkState(group, groupLinkState); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + if (result == null || name != null || description != null || avatarFile != null) { var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile); if (avatarFile != null) { diff --git a/lib/src/main/java/org/asamk/signal/manager/groups/GroupLinkState.java b/lib/src/main/java/org/asamk/signal/manager/groups/GroupLinkState.java new file mode 100644 index 00000000..ad567fbf --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/groups/GroupLinkState.java @@ -0,0 +1,7 @@ +package org.asamk.signal.manager.groups; + +public enum GroupLinkState { + ENABLED, + ENABLED_WITH_APPROVAL, + DISABLED, +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 6dab6dab..a72c158d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -3,6 +3,7 @@ package org.asamk.signal.manager.helper; import com.google.protobuf.InvalidProtocolBufferException; import org.asamk.signal.manager.groups.GroupLinkPassword; +import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.recipients.Profile; @@ -290,6 +291,29 @@ public class GroupV2Helper { return revokeInvites(groupInfoV2, memberUuids); } + public Pair resetGroupLinkPassword(GroupInfoV2 groupInfoV2) throws IOException { + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); + final var newGroupLinkPassword = GroupLinkPassword.createNew().serialize(); + final var change = groupOperations.createModifyGroupLinkPasswordChange(newGroupLinkPassword); + return commitChange(groupInfoV2, change); + } + + public Pair setGroupLinkState( + GroupInfoV2 groupInfoV2, GroupLinkState state + ) throws IOException { + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); + + final var accessRequired = toAccessControl(state); + final var requiresNewPassword = state != GroupLinkState.DISABLED && groupInfoV2.getGroup() + .getInviteLinkPassword() + .isEmpty(); + + final var change = requiresNewPassword ? groupOperations.createModifyGroupLinkPasswordAndRightsChange( + GroupLinkPassword.createNew().serialize(), + accessRequired) : groupOperations.createChangeJoinByLinkRights(accessRequired); + return commitChange(groupInfoV2, change); + } + public GroupChange joinGroup( GroupMasterKey groupMasterKey, GroupLinkPassword groupLinkPassword, @@ -345,6 +369,19 @@ public class GroupV2Helper { return commitChange(groupInfoV2, change); } + private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) { + switch (state) { + case DISABLED: + return AccessControl.AccessRequired.UNSATISFIABLE; + case ENABLED: + return AccessControl.AccessRequired.ANY; + case ENABLED_WITH_APPROVAL: + return AccessControl.AccessRequired.ADMINISTRATOR; + default: + throw new AssertionError(); + } + } + private GroupsV2Operations.GroupOperations getGroupOperations(final GroupInfoV2 groupInfoV2) { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); return groupsV2Operations.forGroup(groupSecretParams); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index 308220cb..fe82862e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -70,7 +70,7 @@ public class GroupInfoV2 extends GroupInfo { @Override public GroupInviteLinkUrl getGroupInviteLink() { - if (this.group == null || this.group.getInviteLinkPassword() == null || ( + if (this.group == null || this.group.getInviteLinkPassword().isEmpty() || ( this.group.getAccessControl().getAddFromInviteLink() != AccessControl.AccessRequired.ANY && this.group.getAccessControl().getAddFromInviteLink() != AccessControl.AccessRequired.ADMINISTRATOR diff --git a/src/main/java/org/asamk/signal/GroupLinkState.java b/src/main/java/org/asamk/signal/GroupLinkState.java new file mode 100644 index 00000000..b0a2510f --- /dev/null +++ b/src/main/java/org/asamk/signal/GroupLinkState.java @@ -0,0 +1,35 @@ +package org.asamk.signal; + +public enum GroupLinkState { + ENABLED { + @Override + public String toString() { + return "enabled"; + } + }, + ENABLED_WITH_APPROVAL { + @Override + public String toString() { + return "enabled-with-approval"; + } + }, + DISABLED { + @Override + public String toString() { + return "disabled"; + } + }; + + public org.asamk.signal.manager.groups.GroupLinkState toLinkState() { + switch (this) { + case ENABLED: + return org.asamk.signal.manager.groups.GroupLinkState.ENABLED; + case ENABLED_WITH_APPROVAL: + return org.asamk.signal.manager.groups.GroupLinkState.ENABLED_WITH_APPROVAL; + case DISABLED: + return org.asamk.signal.manager.groups.GroupLinkState.DISABLED; + default: + throw new AssertionError(); + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 87307446..64485f6d 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -1,9 +1,11 @@ package org.asamk.signal.commands; +import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; +import org.asamk.signal.GroupLinkState; import org.asamk.signal.PlainTextWriterImpl; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; @@ -46,6 +48,12 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { .nargs("*") .help("Specify one or more members to remove group admin privileges"); + subparser.addArgument("--reset-link") + .action(Arguments.storeTrue()) + .help("Reset group link and create new link password"); + subparser.addArgument("--link") + .help("Set group link state, with or without admin approval") + .type(Arguments.enumStringType(GroupLinkState.class)); } @Override @@ -65,16 +73,20 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { var groupDescription = ns.getString("description"); - List groupMembers = ns.getList("member"); + var groupMembers = ns.getList("member"); - List groupRemoveMembers = ns.getList("remove-member"); + var groupRemoveMembers = ns.getList("remove-member"); - List groupAdmins = ns.getList("admin"); + var groupAdmins = ns.getList("admin"); - List groupRemoveAdmins = ns.getList("remove-admin"); + var groupRemoveAdmins = ns.getList("remove-admin"); var groupAvatar = ns.getString("avatar"); + var groupResetLink = ns.getBoolean("reset-link"); + + var groupLinkState = ns.get("link"); + try { if (groupId == null) { var results = m.createGroup(groupName, @@ -91,6 +103,8 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { groupRemoveMembers, groupAdmins, groupRemoveAdmins, + groupResetLink, + groupLinkState != null ? groupLinkState.toLinkState() : null, groupAvatar == null ? null : new File(groupAvatar)); ErrorUtils.handleTimestampAndSendMessageResults(writer, results.first(), results.second()); } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 9b83ace5..834f0d23 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -347,6 +347,8 @@ public class DbusSignalImpl implements Signal { null, null, null, + false, + null, avatar == null ? null : new File(avatar)); checkSendMessageResults(results.first(), results.second()); return groupId; -- 2.51.0 From 7170a68571f2d612f908e5293c15850ba726f35d Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 15 May 2021 14:16:48 +0200 Subject: [PATCH 08/16] Implement setting expiration timer for groups --- .../org/asamk/signal/manager/Manager.java | 47 +++++++++++++------ .../signal/manager/helper/GroupV2Helper.java | 8 ++++ .../signal/commands/UpdateGroupCommand.java | 7 ++- .../org/asamk/signal/dbus/DbusSignalImpl.java | 3 +- 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 03f4e8e9..28e06bbe 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -836,7 +836,8 @@ public class Manager implements Closeable { List removeAdmins, boolean resetGroupLink, GroupLinkState groupLinkState, - File avatarFile + File avatarFile, + Integer expirationTimer ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { return updateGroup(groupId, name, @@ -847,7 +848,8 @@ public class Manager implements Closeable { removeAdmins == null ? null : getSignalServiceAddresses(removeAdmins), resetGroupLink, groupLinkState, - avatarFile); + avatarFile, + expirationTimer); } private Pair> updateGroup( @@ -860,7 +862,8 @@ public class Manager implements Closeable { final Set removeAdmins, final boolean resetGroupLink, final GroupLinkState groupLinkState, - final File avatarFile + final File avatarFile, + Integer expirationTimer ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { var group = getGroupForUpdating(groupId); @@ -874,10 +877,16 @@ public class Manager implements Closeable { removeAdmins, resetGroupLink, groupLinkState, - avatarFile); + avatarFile, + expirationTimer); } - return updateGroupV1((GroupInfoV1) group, name, members, avatarFile); + final var gv1 = (GroupInfoV1) group; + final var result = updateGroupV1(gv1, name, members, avatarFile); + if (expirationTimer != null) { + setExpirationTimer(gv1, expirationTimer); + } + return result; } private Pair> updateGroupV1( @@ -939,7 +948,8 @@ public class Manager implements Closeable { final Set removeAdmins, final boolean resetGroupLink, final GroupLinkState groupLinkState, - final File avatarFile + final File avatarFile, + Integer expirationTimer ) throws IOException { Pair> result = null; if (group.isPendingMember(account.getSelfRecipientId())) { @@ -1010,6 +1020,11 @@ public class Manager implements Closeable { result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); } + if (expirationTimer != null) { + var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + if (result == null || name != null || description != null || avatarFile != null) { var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile); if (avatarFile != null) { @@ -1306,15 +1321,17 @@ public class Manager implements Closeable { /** * Change the expiration timer for a group */ - public void setExpirationTimer(GroupId groupId, int messageExpirationTimer) { - var g = getGroup(groupId); - if (g instanceof GroupInfoV1) { - var groupInfoV1 = (GroupInfoV1) g; - groupInfoV1.messageExpirationTime = messageExpirationTimer; - account.getGroupStore().updateGroup(groupInfoV1); - } else { - throw new RuntimeException("TODO Not implemented!"); - } + private void setExpirationTimer( + GroupInfoV1 groupInfoV1, int messageExpirationTimer + ) throws NotAGroupMemberException, GroupNotFoundException, IOException { + groupInfoV1.messageExpirationTime = messageExpirationTimer; + account.getGroupStore().updateGroup(groupInfoV1); + sendExpirationTimerUpdate(groupInfoV1.getGroupId()); + } + + private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException { + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); + sendGroupMessage(messageBuilder, groupId); } /** diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index a72c158d..b8e7566f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -369,6 +369,14 @@ public class GroupV2Helper { return commitChange(groupInfoV2, change); } + public Pair setMessageExpirationTimer( + GroupInfoV2 groupInfoV2, int messageExpirationTimer + ) throws IOException { + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); + final var change = groupOperations.createModifyGroupTimerChange(messageExpirationTimer); + return commitChange(groupInfoV2, change); + } + private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) { switch (state) { case DISABLED: diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 64485f6d..5a660650 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -54,6 +54,8 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { subparser.addArgument("--link") .help("Set group link state, with or without admin approval") .type(Arguments.enumStringType(GroupLinkState.class)); + + subparser.addArgument("-e", "--expiration").type(int.class).help("Set expiration time of messages (seconds)"); } @Override @@ -87,6 +89,8 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { var groupLinkState = ns.get("link"); + var groupExpiration = ns.getInt("expiration"); + try { if (groupId == null) { var results = m.createGroup(groupName, @@ -105,7 +109,8 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { groupRemoveAdmins, groupResetLink, groupLinkState != null ? groupLinkState.toLinkState() : null, - groupAvatar == null ? null : new File(groupAvatar)); + groupAvatar == null ? null : new File(groupAvatar), + groupExpiration); ErrorUtils.handleTimestampAndSendMessageResults(writer, results.first(), results.second()); } } catch (AttachmentInvalidException e) { diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 834f0d23..6ecf2aba 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -349,7 +349,8 @@ public class DbusSignalImpl implements Signal { null, false, null, - avatar == null ? null : new File(avatar)); + avatar == null ? null : new File(avatar), + null); checkSendMessageResults(results.first(), results.second()); return groupId; } -- 2.51.0 From 78f22c70205bdda075f5db6cd5b77ce0f0f79525 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 15 May 2021 16:37:52 +0200 Subject: [PATCH 09/16] Implement setting group permissions --- .../org/asamk/signal/manager/Manager.java | 23 ++++++++++++- .../manager/groups/GroupPermission.java | 6 ++++ .../signal/manager/helper/GroupV2Helper.java | 32 +++++++++++++++++++ .../org/asamk/signal/GroupPermission.java | 27 ++++++++++++++++ .../signal/commands/QuitGroupCommand.java | 2 +- .../signal/commands/UpdateGroupCommand.java | 25 ++++++++------- .../org/asamk/signal/dbus/DbusSignalImpl.java | 2 ++ 7 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/groups/GroupPermission.java create mode 100644 src/main/java/org/asamk/signal/GroupPermission.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 28e06bbe..c95ab9b7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -25,6 +25,7 @@ import org.asamk.signal.manager.groups.GroupIdV1; 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.GroupUtils; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.helper.GroupV2Helper; @@ -836,6 +837,8 @@ public class Manager implements Closeable { List removeAdmins, boolean resetGroupLink, GroupLinkState groupLinkState, + GroupPermission addMemberPermission, + GroupPermission editDetailsPermission, File avatarFile, Integer expirationTimer ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { @@ -848,6 +851,8 @@ public class Manager implements Closeable { removeAdmins == null ? null : getSignalServiceAddresses(removeAdmins), resetGroupLink, groupLinkState, + addMemberPermission, + editDetailsPermission, avatarFile, expirationTimer); } @@ -862,8 +867,10 @@ public class Manager implements Closeable { final Set removeAdmins, final boolean resetGroupLink, final GroupLinkState groupLinkState, + final GroupPermission addMemberPermission, + final GroupPermission editDetailsPermission, final File avatarFile, - Integer expirationTimer + final Integer expirationTimer ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { var group = getGroupForUpdating(groupId); @@ -877,6 +884,8 @@ public class Manager implements Closeable { removeAdmins, resetGroupLink, groupLinkState, + addMemberPermission, + editDetailsPermission, avatarFile, expirationTimer); } @@ -948,6 +957,8 @@ public class Manager implements Closeable { final Set removeAdmins, final boolean resetGroupLink, final GroupLinkState groupLinkState, + final GroupPermission addMemberPermission, + final GroupPermission editDetailsPermission, final File avatarFile, Integer expirationTimer ) throws IOException { @@ -1020,6 +1031,16 @@ public class Manager implements Closeable { result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); } + if (addMemberPermission != null) { + var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (editDetailsPermission != null) { + var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + if (expirationTimer != null) { var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer); result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); diff --git a/lib/src/main/java/org/asamk/signal/manager/groups/GroupPermission.java b/lib/src/main/java/org/asamk/signal/manager/groups/GroupPermission.java new file mode 100644 index 00000000..00366497 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/groups/GroupPermission.java @@ -0,0 +1,6 @@ +package org.asamk.signal.manager.groups; + +public enum GroupPermission { + EVERY_MEMBER, + ONLY_ADMINS, +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index b8e7566f..06bfdcda 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -4,6 +4,7 @@ import com.google.protobuf.InvalidProtocolBufferException; import org.asamk.signal.manager.groups.GroupLinkPassword; import org.asamk.signal.manager.groups.GroupLinkState; +import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.recipients.Profile; @@ -314,6 +315,26 @@ public class GroupV2Helper { return commitChange(groupInfoV2, change); } + public Pair setEditDetailsPermission( + GroupInfoV2 groupInfoV2, GroupPermission permission + ) throws IOException { + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); + + final var accessRequired = toAccessControl(permission); + final var change = groupOperations.createChangeAttributesRights(accessRequired); + return commitChange(groupInfoV2, change); + } + + public Pair setAddMemberPermission( + GroupInfoV2 groupInfoV2, GroupPermission permission + ) throws IOException { + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); + + final var accessRequired = toAccessControl(permission); + final var change = groupOperations.createChangeMembershipRights(accessRequired); + return commitChange(groupInfoV2, change); + } + public GroupChange joinGroup( GroupMasterKey groupMasterKey, GroupLinkPassword groupLinkPassword, @@ -390,6 +411,17 @@ public class GroupV2Helper { } } + private AccessControl.AccessRequired toAccessControl(final GroupPermission permission) { + switch (permission) { + case EVERY_MEMBER: + return AccessControl.AccessRequired.MEMBER; + case ONLY_ADMINS: + return AccessControl.AccessRequired.ADMINISTRATOR; + default: + throw new AssertionError(); + } + } + private GroupsV2Operations.GroupOperations getGroupOperations(final GroupInfoV2 groupInfoV2) { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); return groupsV2Operations.forGroup(groupSecretParams); diff --git a/src/main/java/org/asamk/signal/GroupPermission.java b/src/main/java/org/asamk/signal/GroupPermission.java new file mode 100644 index 00000000..08056284 --- /dev/null +++ b/src/main/java/org/asamk/signal/GroupPermission.java @@ -0,0 +1,27 @@ +package org.asamk.signal; + +public enum GroupPermission { + EVERY_MEMBER { + @Override + public String toString() { + return "every-member"; + } + }, + ONLY_ADMINS { + @Override + public String toString() { + return "only-admins"; + } + }; + + public org.asamk.signal.manager.groups.GroupPermission toManager() { + switch (this) { + case EVERY_MEMBER: + return org.asamk.signal.manager.groups.GroupPermission.EVERY_MEMBER; + case ONLY_ADMINS: + return org.asamk.signal.manager.groups.GroupPermission.ONLY_ADMINS; + default: + throw new AssertionError(); + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index e47728d5..435d1896 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -33,7 +33,7 @@ public class QuitGroupCommand implements LocalCommand { try { groupId = Util.decodeGroupId(ns.getString("group")); } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id:" + e.getMessage()); + throw new UserErrorException("Invalid group id: " + e.getMessage()); } try { diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 5a660650..6f7f87fd 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -6,6 +6,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; import org.asamk.signal.GroupLinkState; +import org.asamk.signal.GroupPermission; import org.asamk.signal.PlainTextWriterImpl; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; @@ -55,6 +56,13 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { .help("Set group link state, with or without admin approval") .type(Arguments.enumStringType(GroupLinkState.class)); + subparser.addArgument("--set-permission-add-member") + .help("Set permission to add new group members") + .type(Arguments.enumStringType(GroupPermission.class)); + subparser.addArgument("--set-permission-edit-details") + .help("Set permission to edit group details") + .type(Arguments.enumStringType(GroupPermission.class)); + subparser.addArgument("-e", "--expiration").type(int.class).help("Set expiration time of messages (seconds)"); } @@ -67,29 +75,22 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { try { groupId = Util.decodeGroupId(groupIdString); } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id:" + e.getMessage()); + throw new UserErrorException("Invalid group id: " + e.getMessage()); } } var groupName = ns.getString("name"); - var groupDescription = ns.getString("description"); - var groupMembers = ns.getList("member"); - var groupRemoveMembers = ns.getList("remove-member"); - var groupAdmins = ns.getList("admin"); - var groupRemoveAdmins = ns.getList("remove-admin"); - var groupAvatar = ns.getString("avatar"); - var groupResetLink = ns.getBoolean("reset-link"); - var groupLinkState = ns.get("link"); - var groupExpiration = ns.getInt("expiration"); + var groupAddMemberPermission = ns.get("set-permission-add-member"); + var groupEditDetailsPermission = ns.get("set-permission-edit-details"); try { if (groupId == null) { @@ -109,6 +110,8 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { groupRemoveAdmins, groupResetLink, groupLinkState != null ? groupLinkState.toLinkState() : null, + groupAddMemberPermission != null ? groupAddMemberPermission.toManager() : null, + groupEditDetailsPermission != null ? groupEditDetailsPermission.toManager() : null, groupAvatar == null ? null : new File(groupAvatar), groupExpiration); ErrorUtils.handleTimestampAndSendMessageResults(writer, results.first(), results.second()); @@ -134,7 +137,7 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { try { groupId = Util.decodeGroupId(ns.getString("group")).serialize(); } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id:" + e.getMessage()); + throw new UserErrorException("Invalid group id: " + e.getMessage()); } } if (groupId == null) { diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 6ecf2aba..d9ff5996 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -349,6 +349,8 @@ public class DbusSignalImpl implements Signal { null, false, null, + null, + null, avatar == null ? null : new File(avatar), null); checkSendMessageResults(results.first(), results.second()); -- 2.51.0 From ea633efc9c113918c84994f75ed72116f708dada Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 15 May 2021 18:05:07 +0200 Subject: [PATCH 10/16] Prevent last admin from leaving group --- .../java/org/asamk/signal/manager/Manager.java | 18 ++++++++++++++++-- .../groups/LastGroupAdminException.java | 8 ++++++++ .../signal/manager/helper/GroupV2Helper.java | 14 +++++++++++--- .../signal/commands/QuitGroupCommand.java | 16 +++++++++++++++- .../org/asamk/signal/dbus/DbusSignalImpl.java | 8 ++++++-- 5 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/groups/LastGroupAdminException.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index c95ab9b7..889bafb0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -27,6 +27,7 @@ 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.GroupUtils; +import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.helper.GroupV2Helper; import org.asamk.signal.manager.helper.PinHelper; @@ -763,7 +764,9 @@ public class Manager implements Closeable { return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId())); } - public Pair> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException { + public Pair> sendQuitGroupMessage( + GroupId groupId, Set groupAdmins + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, InvalidNumberException, LastGroupAdminException { SignalServiceDataMessage.Builder messageBuilder; final var g = getGroupForUpdating(groupId); @@ -775,7 +778,18 @@ public class Manager implements Closeable { account.getGroupStore().updateGroup(groupInfoV1); } else { final var groupInfoV2 = (GroupInfoV2) g; - final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2); + final var currentAdmins = g.getAdminMembers(); + final var newAdmins = getSignalServiceAddresses(groupAdmins); + newAdmins.removeAll(currentAdmins); + newAdmins.retainAll(g.getMembers()); + if (currentAdmins.contains(getSelfRecipientId()) + && currentAdmins.size() == 1 + && g.getMembers().size() > 1 + && newAdmins.size() == 0) { + // Last admin can't leave the group, unless she's also the last member + throw new LastGroupAdminException(g.getGroupId(), g.getTitle()); + } + final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins); groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient); messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); account.getGroupStore().updateGroup(groupInfoV2); diff --git a/lib/src/main/java/org/asamk/signal/manager/groups/LastGroupAdminException.java b/lib/src/main/java/org/asamk/signal/manager/groups/LastGroupAdminException.java new file mode 100644 index 00000000..28c52fad --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/groups/LastGroupAdminException.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager.groups; + +public class LastGroupAdminException extends Exception { + + public LastGroupAdminException(GroupId groupId, String groupName) { + super("User is last admin in group: " + groupName + " (" + groupId.toBase64() + ")"); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 06bfdcda..32c61a84 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -250,7 +250,9 @@ public class GroupV2Helper { return commitChange(groupInfoV2, change); } - public Pair leaveGroup(GroupInfoV2 groupInfoV2) throws IOException { + public Pair leaveGroup( + GroupInfoV2 groupInfoV2, Set membersToMakeAdmin + ) throws IOException { var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList(); final var selfUuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId()) .getUuid() @@ -259,9 +261,15 @@ public class GroupV2Helper { if (selfPendingMember.isPresent()) { return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get())); - } else { - return ejectMembers(groupInfoV2, Set.of(selfUuid)); } + + final var adminUuids = membersToMakeAdmin.stream() + .map(addressResolver::resolveSignalServiceAddress) + .map(SignalServiceAddress::getUuid) + .map(Optional::get) + .collect(Collectors.toList()); + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); + return commitChange(groupInfoV2, groupOperations.createLeaveAndPromoteMembersToAdmin(selfUuid, adminUuids)); } public Pair removeMembers( diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 435d1896..6bc30698 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -11,10 +11,14 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.Util; +import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; +import java.util.HashSet; +import java.util.Set; import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResults; @@ -23,6 +27,9 @@ public class QuitGroupCommand implements LocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.addArgument("-g", "--group").required(true).help("Specify the recipient group ID."); + subparser.addArgument("--admin") + .nargs("*") + .help("Specify one or more members to make a group admin, required if you're currently the only admin."); } @Override @@ -36,13 +43,20 @@ public class QuitGroupCommand implements LocalCommand { throw new UserErrorException("Invalid group id: " + e.getMessage()); } + var groupAdmins = ns.getList("admin"); + try { - final var results = m.sendQuitGroupMessage(groupId); + final var results = m.sendQuitGroupMessage(groupId, + groupAdmins == null ? Set.of() : new HashSet<>(groupAdmins)); handleTimestampAndSendMessageResults(writer, results.first(), results.second()); } catch (IOException e) { throw new IOErrorException("Failed to send message: " + e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); + } catch (InvalidNumberException e) { + throw new UserErrorException("Failed to parse admin number: " + e.getMessage()); + } catch (LastGroupAdminException e) { + throw new UserErrorException("You need to specify a new admin with --admin: " + e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index d9ff5996..3e06d613 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -8,6 +8,7 @@ import org.asamk.signal.manager.NotMasterDeviceException; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.util.ErrorUtils; @@ -24,6 +25,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -442,11 +444,13 @@ public class DbusSignalImpl implements Signal { public void quitGroup(final byte[] groupId) { var group = GroupId.unknownVersion(groupId); try { - m.sendQuitGroupMessage(group); + m.sendQuitGroupMessage(group, Set.of()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new Error.GroupNotFound(e.getMessage()); - } catch (IOException e) { + } catch (IOException | LastGroupAdminException e) { throw new Error.Failure(e.getMessage()); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); } } -- 2.51.0 From 81ee2c2d2bb80660d85cf9d1b7ecdf001983b3d6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 15 May 2021 18:26:45 +0200 Subject: [PATCH 11/16] Update group with remaining options, after creating it --- .../org/asamk/signal/manager/Manager.java | 2 +- .../signal/commands/UpdateGroupCommand.java | 37 +++++++++++-------- .../org/asamk/signal/dbus/DbusSignalImpl.java | 4 +- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 889bafb0..f285e2ef 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1060,7 +1060,7 @@ public class Manager implements Closeable { result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); } - if (result == null || name != null || description != null || avatarFile != null) { + if (name != null || description != null || avatarFile != null) { var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile); if (avatarFile != null) { avatarStore.storeGroupAvatar(group.getGroupId(), diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 6f7f87fd..61d5a68a 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -98,22 +98,27 @@ public class UpdateGroupCommand implements DbusCommand, LocalCommand { groupMembers, groupAvatar == null ? null : new File(groupAvatar)); ErrorUtils.handleTimestampAndSendMessageResults(writer, 0, results.second()); - final var newGroupId = results.first(); - writer.println("Created new group: \"{}\"", newGroupId.toBase64()); - } else { - var results = m.updateGroup(groupId, - groupName, - groupDescription, - groupMembers, - groupRemoveMembers, - groupAdmins, - groupRemoveAdmins, - groupResetLink, - groupLinkState != null ? groupLinkState.toLinkState() : null, - groupAddMemberPermission != null ? groupAddMemberPermission.toManager() : null, - groupEditDetailsPermission != null ? groupEditDetailsPermission.toManager() : null, - groupAvatar == null ? null : new File(groupAvatar), - groupExpiration); + groupId = results.first(); + writer.println("Created new group: \"{}\"", groupId.toBase64()); + groupName = null; + groupMembers = null; + groupAvatar = null; + } + + var results = m.updateGroup(groupId, + groupName, + groupDescription, + groupMembers, + groupRemoveMembers, + groupAdmins, + groupRemoveAdmins, + groupResetLink, + groupLinkState != null ? groupLinkState.toLinkState() : null, + groupAddMemberPermission != null ? groupAddMemberPermission.toManager() : null, + groupEditDetailsPermission != null ? groupEditDetailsPermission.toManager() : null, + groupAvatar == null ? null : new File(groupAvatar), + groupExpiration); + if (results != null) { ErrorUtils.handleTimestampAndSendMessageResults(writer, results.first(), results.second()); } } catch (AttachmentInvalidException e) { diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 3e06d613..2c3871ab 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -355,7 +355,9 @@ public class DbusSignalImpl implements Signal { null, avatar == null ? null : new File(avatar), null); - checkSendMessageResults(results.first(), results.second()); + if (results != null) { + checkSendMessageResults(results.first(), results.second()); + } return groupId; } } catch (IOException e) { -- 2.51.0 From 761baf72063adfb15d99d39bacce3ebdfbe87d0d Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 15 May 2021 18:40:16 +0200 Subject: [PATCH 12/16] Update tests --- graalvm-config-dir/reflect-config.json | 38 ++++++++++++++++++++++++++ run_tests.sh | 16 +++++++---- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index db7e24f6..7e21bd3f 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -1091,6 +1091,26 @@ "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$DeleteRequestingMemberAction", "fields":[{"name":"deletedUserId_", "allowUnsafeAccess":true}] }, +{ + "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyAddFromInviteLinkAccessControlAction", + "fields":[{"name":"addFromInviteLinkAccess_", "allowUnsafeAccess":true}] +}, +{ + "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyAttributesAccessControlAction", + "fields":[{"name":"attributesAccess_", "allowUnsafeAccess":true}] +}, +{ + "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyDescriptionAction", + "fields":[{"name":"description_", "allowUnsafeAccess":true}] +}, +{ + "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyDisappearingMessagesTimerAction", + "fields":[{"name":"timer_", "allowUnsafeAccess":true}] +}, +{ + "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyInviteLinkPasswordAction", + "fields":[{"name":"inviteLinkPassword_", "allowUnsafeAccess":true}] +}, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyMemberProfileKeyAction", "fields":[{"name":"presentation_", "allowUnsafeAccess":true}] @@ -1102,6 +1122,10 @@ {"name":"userId_", "allowUnsafeAccess":true} ] }, +{ + "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyMembersAccessControlAction", + "fields":[{"name":"membersAccess_", "allowUnsafeAccess":true}] +}, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyTitleAction", "fields":[{"name":"title_", "allowUnsafeAccess":true}] @@ -1117,6 +1141,20 @@ {"name":"userId_", "allowUnsafeAccess":true} ] }, +{ + "name":"org.signal.storageservice.protos.groups.GroupInviteLink", + "fields":[ + {"name":"contentsCase_", "allowUnsafeAccess":true}, + {"name":"contents_", "allowUnsafeAccess":true} + ] +}, +{ + "name":"org.signal.storageservice.protos.groups.GroupInviteLink$GroupInviteLinkContentsV1", + "fields":[ + {"name":"groupMasterKey_", "allowUnsafeAccess":true}, + {"name":"inviteLinkPassword_", "allowUnsafeAccess":true} + ] +}, { "name":"org.signal.storageservice.protos.groups.Member", "fields":[ diff --git a/run_tests.sh b/run_tests.sh index 44a190fb..3df13530 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -79,20 +79,21 @@ sleep 5 run_main -u "$NUMBER_1" setPin "$TEST_PIN_1" run_main -u "$NUMBER_2" removePin -## Identities -run_main -u "$NUMBER_1" listIdentities -run_main -u "$NUMBER_2" trust "$NUMBER_1" -a - ## Contacts run_main -u "$NUMBER_2" updateContact "$NUMBER_1" -n NUMBER_1 -e 10 run_main -u "$NUMBER_2" block "$NUMBER_1" run_main -u "$NUMBER_2" unblock "$NUMBER_1" run_main -u "$NUMBER_2" listContacts +run_main -u "$NUMBER_1" send "$NUMBER_2" -m hi +run_main -u "$NUMBER_2" receive +run_main -u "$NUMBER_2" send "$NUMBER_1" -m hi +run_main -u "$NUMBER_1" receive +run_main -u "$NUMBER_2" receive ## Groups GROUP_ID=$(run_main -u "$NUMBER_1" updateGroup -n GRUPPE -a LICENSE -m "$NUMBER_1" | grep -oP '(?<=").+(?=")') run_main -u "$NUMBER_1" send "$NUMBER_2" -m first -run_main -u "$NUMBER_1" updateGroup -g "$GROUP_ID" -n GRUPPE_UMB -m "$NUMBER_2" +run_main -u "$NUMBER_1" updateGroup -g "$GROUP_ID" -n GRUPPE_UMB -m "$NUMBER_2" --admin "$NUMBER_2" --remove-admin "$NUMBER_2" --description DESCRIPTION --link=enabled-with-approval --set-permission-add-member=only-admins --set-permission-edit-details=only-admins -e 42 run_main -u "$NUMBER_1" listGroups -d run_main -u "$NUMBER_1" --output=json listGroups -d run_main -u "$NUMBER_2" --verbose receive @@ -104,6 +105,11 @@ run_main -u "$NUMBER_1" updateGroup -g "$GROUP_ID" -m "$NUMBER_2" run_main -u "$NUMBER_1" block "$GROUP_ID" run_main -u "$NUMBER_1" unblock "$GROUP_ID" +## Identities +run_main -u "$NUMBER_1" listIdentities +run_main -u "$NUMBER_2" listIdentities +run_main -u "$NUMBER_2" trust "$NUMBER_1" -a + ## Basic send/receive for OUTPUT in "plain-text" "json"; do run_main -u "$NUMBER_1" --output="$OUTPUT" getUserStatus "$NUMBER_1" "$NUMBER_2" "+111111111" -- 2.51.0 From 5c3fc44d00cb7b18c4c5b4b6b5d7fe09f18973db Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 15 May 2021 18:53:00 +0200 Subject: [PATCH 13/16] Update documentation --- CHANGELOG.md | 9 ++++++++- man/signal-cli.1.adoc | 30 +++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7515a984..26b0cedb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,16 @@ # Changelog ## [Unreleased] +**Attention**: Now requires native libsignal-client version 0.5.1 + +### Added +- Added new parameters to `updateGroup` for group v2 features: + `--remove-member`, `--admin`, `--remove-admin`, `--reset-link`, `--link`, `--set-permission-add-member`, `--set-permission-edit-details`, `--expiration` + +### Fixed +- Prevent last admin of a group from leaving the group ## [0.8.3] - 2021-05-13 -**Attention**: Now requires native libsignal-client version 0.5.1 ### Fixed - Upgrading from account files with older profiles diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 7c2e9ea6..b9a7a9cb 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -243,12 +243,40 @@ If not specified, a new group with a new random ID is generated. *-n* NAME, *--name* NAME:: Specify the new group name. +*-d* DESCRIPTION, *--description* DESCRIPTION:: +Specify the new group description. + *-a* AVATAR, *--avatar* AVATAR:: Specify a new group avatar image file. *-m* [MEMBER [MEMBER ...]], *--member* [MEMBER [MEMBER ...]]:: Specify one or more members to add to the group. +*-r* [MEMBER [MEMBER ...]], *--remove-member* [MEMBER [MEMBER ...]]:: +Specify one or more members to remove from the group + +*--admin* [MEMBER [MEMBER ...]]:: +Specify one or more members to make a group admin + +*--remove-admin* [MEMBER [MEMBER ...]]:: +Specify one or more members to remove group admin privileges + +*--reset-link*:: +Reset group link and create new link password + +*--link* LINK_STATE:: +Set group link state: `enabled`, `enabled-with-approval`, `disabled` + +*--set-permission-add-member* PERMISSION:: +Set permission to add new group members: `every-member`, `only-admins` + +*--set-permission-edit-details* PERMISSION:: +Set permission to edit group details: `every-member`, `only-admins` + +*-e* EXPIRATION_SECONDS, *--expiration* EXPIRATION_SECONDS:: +Set expiration time of messages (seconds). +To disable expiration set expiration time to 0. + === quitGroup Send a quit group message to all group members and remove self from member list. @@ -324,7 +352,7 @@ Specify the contact phone number. *-n*, *--name*:: Specify the new name for this contact. -*-e*, *--expiration*:: +*-e*, *--expiration* EXPIRATION_SECONDS:: Set expiration time of messages (seconds). To disable expiration set expiration time to 0. -- 2.51.0 From 109ce26780e48d6a40efe1fc6cdb01a1f0ae1160 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 16 May 2021 16:11:15 +0200 Subject: [PATCH 14/16] Update tests --- graalvm-config-dir/reflect-config.json | 7 +++++++ run_tests.sh | 2 ++ 2 files changed, 9 insertions(+) diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index 7e21bd3f..41036f2e 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -1231,6 +1231,13 @@ {"name":"uuid_", "allowUnsafeAccess":true} ] }, +{ + "name":"org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole", + "fields":[ + {"name":"role_", "allowUnsafeAccess":true}, + {"name":"uuid_", "allowUnsafeAccess":true} + ] +}, { "name":"org.signal.storageservice.protos.groups.local.DecryptedPendingMember", "fields":[ diff --git a/run_tests.sh b/run_tests.sh index 3df13530..c4f5a228 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -136,6 +136,8 @@ run_main -u "$NUMBER_1" listDevices run_linked -u "$NUMBER_1" sendSyncRequest run_main -u "$NUMBER_1" sendContacts +run_main -u "$NUMBER_1" removeDevice -d 2 + ## DBus #run_main -u "$NUMBER_1" --dbus send "$NUMBER_2" -m daemon_not_running #run_main daemon & -- 2.51.0 From 2016cf2a5fdf83310d78ca1672a313884880f26e Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 16 May 2021 16:11:55 +0200 Subject: [PATCH 15/16] Fix camel case device-id flag --- man/signal-cli.1.adoc | 2 +- .../java/org/asamk/signal/commands/RemoveDeviceCommand.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index b9a7a9cb..ed498eaf 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -142,7 +142,7 @@ Show a list of connected devices. Remove a connected device. Only works, if this is the master device. -*-d* DEVICEID, *--deviceId* DEVICEID:: +*-d* DEVICEID, *--device-id* DEVICEID:: Specify the device you want to remove. Use listDevices to see the deviceIds. diff --git a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java index c9be92e8..6185cafc 100644 --- a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java @@ -13,7 +13,7 @@ public class RemoveDeviceCommand implements LocalCommand { @Override public void attachToSubparser(final Subparser subparser) { - subparser.addArgument("-d", "--deviceId") + subparser.addArgument("-d", "--device-id", "--deviceId") .type(int.class) .required(true) .help("Specify the device you want to remove. Use listDevices to see the deviceIds."); @@ -22,7 +22,7 @@ public class RemoveDeviceCommand implements LocalCommand { @Override public void handleCommand(final Namespace ns, final Manager m) throws CommandException { try { - int deviceId = ns.getInt("deviceId"); + int deviceId = ns.getInt("device-id"); m.removeLinkedDevices(deviceId); } catch (IOException e) { throw new IOErrorException("Error while removing device: " + e.getMessage()); -- 2.51.0 From f445cfb5c16ff0de5c96a9f96f5213e2cf5e770f Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 16 May 2021 16:53:04 +0200 Subject: [PATCH 16/16] Include proof required information in sending error --- src/main/java/org/asamk/signal/util/ErrorUtils.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 100368f6..2442ddb6 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -6,9 +6,11 @@ import org.asamk.signal.commands.exceptions.IOErrorException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; public class ErrorUtils { @@ -46,6 +48,17 @@ public class ErrorUtils { return String.format("Unregistered user \"%s\"", result.getAddress().getLegacyIdentifier()); } else if (result.getIdentityFailure() != null) { return String.format("Untrusted Identity for \"%s\"", result.getAddress().getLegacyIdentifier()); + } else if (result.getProofRequiredFailure() != null) { + final var failure = result.getProofRequiredFailure(); + return String.format( + "CAPTCHA proof required for sending to \"%s\", available options \"%s\" with token \"%s\", or wait \"%d\" seconds", + result.getAddress().getLegacyIdentifier(), + failure.getOptions() + .stream() + .map(ProofRequiredException.Option::toString) + .collect(Collectors.joining(", ")), + failure.getToken(), + failure.getRetryAfterSeconds()); } return null; } -- 2.51.0