From aa1ed9e2331a1cc972994b1d805fcec8f90cc462 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 28 Feb 2026 11:59:31 +0100 Subject: [PATCH] Add support for sending adminDelete messages --- .../org/asamk/signal/manager/Manager.java | 8 ++ .../signal/manager/internal/ManagerImpl.java | 23 +++++ man/signal-cli.1.adoc | 21 +++++ .../org/asamk/signal/commands/Commands.java | 1 + .../commands/SendAdminDeleteCommand.java | 90 +++++++++++++++++++ .../asamk/signal/dbus/DbusManagerImpl.java | 11 +++ 6 files changed, 154 insertions(+) create mode 100644 src/main/java/org/asamk/signal/commands/SendAdminDeleteCommand.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 5d5fe381..e6a3ae3b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -224,6 +224,14 @@ public interface Manager extends Closeable { final boolean isStory ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException; + SendMessageResults sendAdminDelete( + RecipientIdentifier.Single targetAuthor, + long targetSentTimestamp, + Set recipients, + boolean notifySelf, + boolean isStory + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException; + SendMessageResults sendPinMessage( int pinDuration, RecipientIdentifier.Single targetAuthor, 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 6306cbf2..c3471ed2 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 @@ -993,6 +993,29 @@ public class ManagerImpl implements Manager { return sendMessage(messageBuilder, recipients, notifySelf); } + @Override + public SendMessageResults sendAdminDelete( + RecipientIdentifier.Single targetAuthor, + long targetSentTimestamp, + Set recipients, + final boolean notifySelf, + final boolean isStory + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException { + final var targetAuthorRecipientId = context.getRecipientHelper().resolveRecipient(targetAuthor); + final var authorServiceId = context.getRecipientHelper() + .resolveSignalServiceAddress(targetAuthorRecipientId) + .getServiceId(); + final var adminDelete = new SignalServiceDataMessage.AdminDelete(authorServiceId, targetSentTimestamp); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withAdminDelete(adminDelete); + if (isStory) { + messageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(authorServiceId, + targetSentTimestamp)); + } + return sendMessage(messageBuilder, + recipients.stream().map(r -> (RecipientIdentifier) r).collect(Collectors.toSet()), + notifySelf); + } + @Override public SendMessageResults sendPinMessage( int pinDuration, diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index cfad70ed..1393ca4b 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -287,6 +287,27 @@ One or more numbers to check. [--username [USERNAME ...]]:: One or more usernames or username links to check. +=== sendAdminDelete + +Send admin delete message for a previously received or sent message. +Admin delete is used by group admins to remove messages from all group members. +Only works with group recipients. + +*-g* GROUP, *--group-id* GROUP:: +Specify the recipient group ID in base64 encoding (required). + +*--notify-self*:: +If self is part of recipients/groups send a normal message, not a sync message. + +*-a* RECIPIENT, *--target-author* RECIPIENT:: +Specify the number of the author of the message to admin delete. + +*-t* TIMESTAMP, *--target-timestamp* TIMESTAMP:: +Specify the timestamp of the message to admin delete. + +*--story*:: +Admin delete a story instead of a normal message. + === send Send a message to another user or group. diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index a5d827f4..d1f717b3 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -37,6 +37,7 @@ public class Commands { addCommand(new RemoveDeviceCommand()); addCommand(new RemovePinCommand()); addCommand(new RemoteDeleteCommand()); + addCommand(new SendAdminDeleteCommand()); addCommand(new SendCommand()); addCommand(new SendContactsCommand()); addCommand(new SendMessageRequestResponseCommand()); diff --git a/src/main/java/org/asamk/signal/commands/SendAdminDeleteCommand.java b/src/main/java/org/asamk/signal/commands/SendAdminDeleteCommand.java new file mode 100644 index 00000000..11cf3d3b --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SendAdminDeleteCommand.java @@ -0,0 +1,90 @@ +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.commands.exceptions.CommandException; +import org.asamk.signal.commands.exceptions.UnexpectedErrorException; +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.GroupSendingNotAllowedException; +import org.asamk.signal.manager.api.NotAGroupMemberException; +import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.api.UnregisteredRecipientException; +import org.asamk.signal.output.OutputWriter; +import org.asamk.signal.util.CommandUtil; + +import java.io.IOException; +import java.util.Set; + +import static org.asamk.signal.util.SendMessageResultUtils.outputResult; + +public class SendAdminDeleteCommand implements JsonRpcLocalCommand { + + @Override + public String getName() { + return "sendAdminDelete"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help("Send admin delete message for a previously received or sent message."); + subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID.").nargs("+"); + subparser.addArgument("--notify-self") + .help("If self is part of recipients/groups send a normal message, not a sync message.") + .action(Arguments.storeTrue()); + + subparser.addArgument("-a", "--target-author") + .required(true) + .help("Specify the number of the author of the message to admin delete."); + subparser.addArgument("-t", "--target-timestamp") + .required(true) + .type(long.class) + .help("Specify the timestamp of the message to admin delete."); + subparser.addArgument("--story") + .help("Admin delete a story instead of a normal message") + .action(Arguments.storeTrue()); + } + + @Override + public void handleCommand( + final Namespace ns, + final Manager m, + final OutputWriter outputWriter + ) throws CommandException { + final var notifySelf = Boolean.TRUE.equals(ns.getBoolean("notify-self")); + final var groupIdStrings = ns.getList("group-id"); + + Set groupIdentifiers = CommandUtil.getGroupIdentifiers(groupIdStrings); + + if (groupIdentifiers.isEmpty()) { + throw new UserErrorException("Admin delete requires group IDs"); + } + + final var targetAuthor = ns.getString("target-author"); + final var targetTimestamp = ns.getLong("target-timestamp"); + final var isStory = Boolean.TRUE.equals(ns.getBoolean("story")); + + final RecipientIdentifier.Single targetAuthorIdentifier = CommandUtil.getSingleRecipientIdentifier(targetAuthor, + m.getSelfNumber()); + + try { + final var results = m.sendAdminDelete(targetAuthorIdentifier, + targetTimestamp, + groupIdentifiers, + notifySelf, + isStory); + outputResult(outputWriter, results); + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new UserErrorException(e.getMessage()); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")", e); + } catch (UnregisteredRecipientException e) { + throw new UserErrorException("The user " + e.getSender().getIdentifier() + " is not registered."); + } + } +} + diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index d9018bfd..c0d4eb9f 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -493,6 +493,17 @@ public class DbusManagerImpl implements Manager { groupId)); } + @Override + public SendMessageResults sendAdminDelete( + final RecipientIdentifier.Single targetAuthor, + final long targetSentTimestamp, + final Set recipients, + final boolean notifySelf, + final boolean isStory + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException { + throw new UnsupportedOperationException(); + } + @Override public SendMessageResults sendPinMessage( final int pinDuration,