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 80f24e39..7d0c73af 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -184,6 +184,10 @@ public interface Manager extends Closeable { void deleteGroup(GroupId groupId) throws IOException; + SendGroupMessageResults terminateGroup( + GroupId groupId + ) throws IOException, GroupNotFoundException, NotAGroupMemberException; + Pair createGroup( String name, Set members, 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 1c750358..7d689c34 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 @@ -363,6 +363,28 @@ public class GroupHelper { return results; } + public SendGroupMessageResults terminateGroup(final GroupId groupId) throws IOException, GroupNotFoundException, NotAGroupMemberException { + final var group = getGroupForUpdating(groupId); + if (!(group instanceof GroupInfoV2)) { + throw new IOException("Terminating a group is only supported for Signal group v2 groups."); + } + + SendGroupMessageResults results; + try { + results = terminateGroupV2((GroupInfoV2) group); + } catch (ConflictException e) { + // Detected conflicting update, refreshing group and trying again + results = terminateGroupV2((GroupInfoV2) getGroup(groupId, true)); + } + context.getJobExecutor().enqueueJob(new SyncStorageJob()); + return results; + } + + private SendGroupMessageResults terminateGroupV2(final GroupInfoV2 group) throws IOException { + final var groupGroupChangePair = context.getGroupV2Helper().terminateGroup(group); + return sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + public void updateGroupProfileKey(GroupIdV2 groupId) throws GroupNotFoundException, NotAGroupMemberException, IOException { var group = getGroupForUpdating(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 888dbb1c..83e949b5 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 @@ -543,6 +543,14 @@ class GroupV2Helper { return commitChange(groupInfoV2, change); } + Pair terminateGroup( + GroupInfoV2 groupInfoV2 + ) throws IOException { + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); + final var change = groupOperations.createTerminateGroup(); + return commitChange(groupInfoV2, change); + } + Pair setMemberLabels( GroupInfoV2 groupInfoV2, String labelEmoji, diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java index 5dec030b..34ba3586 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java @@ -610,6 +610,11 @@ public class ManagerImpl implements Manager { context.getGroupHelper().deleteGroup(groupId); } + @Override + public SendGroupMessageResults terminateGroup(GroupId groupId) throws IOException, GroupNotFoundException, NotAGroupMemberException { + return context.getGroupHelper().terminateGroup(groupId); + } + @Override public Pair createGroup( String name, diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 8d8c94a0..34d102b7 100644 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -503,6 +503,11 @@ Exceptions: Failure quitGroup() -> <>:: Exceptions: Failure, LastGroupAdmin +terminateGroup() -> <>:: +Permanently terminate the group for all members (requires admin privileges). Afterwards nobody can send messages or start calls; members keep access to the existing history. + +Exceptions: Failure + removeAdmins(recipients) -> <>:: * recipients : String array of phone numbers diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 217f9f69..6fcdfee1 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -786,6 +786,16 @@ Specify the recipient group ID in base64 encoding. *--delete*:: Delete local group data completely after quitting group. +=== terminateGroup + +Permanently terminate a group for all members. +This requires admin privileges. +Afterwards no member (not even an admin) can send messages or start calls in the group; members keep access to the existing message history. +Only supported for Signal group v2 groups. + +*-g* GROUP, *--group-id* GROUP:: +Specify the group ID in base64 encoding. + === listGroups Show a list of known groups and related information. diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 2fa10457..728ff4c6 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -612,6 +612,8 @@ public interface Signal extends DBusInterface { void deleteGroup() throws Error.Failure; + void terminateGroup() throws Error.Failure; + void addMembers(List recipients) throws Error.Failure; void removeMembers(List recipients) throws Error.Failure; diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 4787c7d1..a57c8c97 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -61,6 +61,7 @@ public class Commands { addCommand(new SubmitRateLimitChallengeCommand()); addCommand(new StartChangeNumberCommand()); addCommand(new StartLinkCommand()); + addCommand(new TerminateGroupCommand()); addCommand(new TrustCommand()); addCommand(new UnblockCommand()); addCommand(new UnregisterCommand()); diff --git a/src/main/java/org/asamk/signal/commands/TerminateGroupCommand.java b/src/main/java/org/asamk/signal/commands/TerminateGroupCommand.java new file mode 100644 index 00000000..8161e614 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/TerminateGroupCommand.java @@ -0,0 +1,54 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.signal.commands.exceptions.CommandException; +import org.asamk.signal.commands.exceptions.IOErrorException; +import org.asamk.signal.commands.exceptions.UserErrorException; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.api.GroupNotFoundException; +import org.asamk.signal.manager.api.NotAGroupMemberException; +import org.asamk.signal.output.OutputWriter; +import org.asamk.signal.util.CommandUtil; + +import java.io.IOException; + +import static org.asamk.signal.util.SendMessageResultUtils.outputResult; + +public class TerminateGroupCommand implements JsonRpcLocalCommand { + + @Override + public String getName() { + return "terminateGroup"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help( + "Permanently terminate a group for all members. Requires admin privileges; afterwards nobody can send messages or start calls."); + subparser.addArgument("-g", "--group-id", "--group").required(true).help("Specify the group ID."); + } + + @Override + public void handleCommand( + final Namespace ns, + final Manager m, + final OutputWriter outputWriter + ) throws CommandException { + final var groupId = CommandUtil.getGroupId(ns.getString("group-id")); + + try { + final var results = m.terminateGroup(groupId); + outputResult(outputWriter, results); + } catch (IOException e) { + throw new IOErrorException("Failed to send message: " + + e.getMessage() + + " (" + + e.getClass().getSimpleName() + + ")", e); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new UserErrorException("Failed to terminate group: " + e.getMessage()); + } + } +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index b93d0920..8467c996 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -327,6 +327,21 @@ public class DbusManagerImpl implements Manager { group.deleteGroup(); } + @Override + public SendGroupMessageResults terminateGroup( + final GroupId groupId + ) throws IOException, GroupNotFoundException, NotAGroupMemberException { + final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); + try { + group.terminateGroup(); + } catch (Signal.Error.GroupNotFound e) { + throw new GroupNotFoundException(groupId); + } catch (Signal.Error.NotAGroupMember e) { + throw new NotAGroupMemberException(groupId, group.Get("org.asamk.Signal.Group", "Name")); + } + return new SendGroupMessageResults(0, List.of()); + } + @Override public Pair createGroup( final String name, diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 7c67ece2..1d53fe7d 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -1376,6 +1376,19 @@ public class DbusSignalImpl implements Signal, AutoCloseable { updateGroups(); } + @Override + public void terminateGroup() throws Error.Failure { + try { + m.terminateGroup(groupId); + } catch (GroupNotFoundException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (NotAGroupMemberException e) { + throw new Error.NotAGroupMember(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + @Override public void addMembers(final List recipients) throws Error.Failure { final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());