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 2875ee15..5d5fe381 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,23 @@ public interface Manager extends Closeable { final boolean isStory ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException; + SendMessageResults sendPinMessage( + int pinDuration, + RecipientIdentifier.Single targetAuthor, + long targetSentTimestamp, + Set recipients, + boolean notifySelf, + boolean isStory + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException; + + SendMessageResults sendUnpinMessage( + RecipientIdentifier.Single targetAuthor, + long targetSentTimestamp, + Set recipients, + boolean notifySelf, + boolean isStory + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException; + SendMessageResults sendPaymentNotificationMessage( byte[] receipt, String note, 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 f8db4305..6306cbf2 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,54 @@ public class ManagerImpl implements Manager { return sendMessage(messageBuilder, recipients, notifySelf); } + @Override + public SendMessageResults sendPinMessage( + int pinDuration, + 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 duration = pinDuration >= 0 ? pinDuration : null; + final var forever = pinDuration < 0; + final var pinnedMessage = new SignalServiceDataMessage.PinnedMessage(authorServiceId, + targetSentTimestamp, + duration, + forever); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withPinnedMessage(pinnedMessage); + if (isStory) { + messageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(authorServiceId, + targetSentTimestamp)); + } + return sendMessage(messageBuilder, recipients, notifySelf); + } + + @Override + public SendMessageResults sendUnpinMessage( + 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 unpinnedMessage = new SignalServiceDataMessage.UnpinnedMessage(authorServiceId, targetSentTimestamp); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withUnpinnedMessage(unpinnedMessage); + if (isStory) { + messageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(authorServiceId, + targetSentTimestamp)); + } + return sendMessage(messageBuilder, recipients, notifySelf); + } + @Override public SendMessageResults sendPaymentNotificationMessage( byte[] receipt, diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 28a468fe..cfad70ed 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -497,6 +497,60 @@ The base64 encoded receipt blob. *--note* NOTE:: Specify a note for the payment notification. +=== sendPinMessage + +Send pin message for a previously received or sent message. + +RECIPIENT:: +Specify the recipients. + +*-g* GROUP, *--group-id* GROUP:: +Specify the recipient group ID in base64 encoding. + +*-u* USERNAME, *--username* USERNAME:: +Specify the recipient username or username link. + +*--note-to-self*:: +Send the pin message to self. + +*--notify-self*:: +If self is part of recipients/groups send a normal message, not a sync message. + +*-d* DURATION, *--pin-duration* DURATION:: +Specify the pin duration in seconds. +Use -1 for forever (default: -1). + +*-a* RECIPIENT, *--target-author* RECIPIENT:: +Specify the number of the author of the message to pin. + +*-t* TIMESTAMP, *--target-timestamp* TIMESTAMP:: +Specify the timestamp of the message to pin. + +=== sendUnpinMessage + +Send unpin message for a previously received or sent message. + +RECIPIENT:: +Specify the recipients. + +*-g* GROUP, *--group-id* GROUP:: +Specify the recipient group ID in base64 encoding. + +*-u* USERNAME, *--username* USERNAME:: +Specify the recipient username or username link. + +*--note-to-self*:: +Send the unpin message to self. + +*--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 unpin. + +*-t* TIMESTAMP, *--target-timestamp* TIMESTAMP:: +Specify the timestamp of the message to unpin. + === sendReaction Send reaction to a previously received or sent message. diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 052de562..a5d827f4 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -41,6 +41,7 @@ public class Commands { addCommand(new SendContactsCommand()); addCommand(new SendMessageRequestResponseCommand()); addCommand(new SendPaymentNotificationCommand()); + addCommand(new SendPinMessageCommand()); addCommand(new SendPollCreateCommand()); addCommand(new SendPollVoteCommand()); addCommand(new SendPollTerminateCommand()); @@ -48,6 +49,7 @@ public class Commands { addCommand(new SendReceiptCommand()); addCommand(new SendSyncRequestCommand()); addCommand(new SendTypingCommand()); + addCommand(new SendUnpinMessageCommand()); addCommand(new SetPinCommand()); addCommand(new SubmitRateLimitChallengeCommand()); addCommand(new StartChangeNumberCommand()); diff --git a/src/main/java/org/asamk/signal/commands/SendPinMessageCommand.java b/src/main/java/org/asamk/signal/commands/SendPinMessageCommand.java new file mode 100644 index 00000000..88a39626 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SendPinMessageCommand.java @@ -0,0 +1,104 @@ +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 static org.asamk.signal.util.SendMessageResultUtils.outputResult; + +public class SendPinMessageCommand implements JsonRpcLocalCommand { + + @Override + public String getName() { + return "sendPinMessage"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help("Send pin message for a previously received or sent message."); + subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID.").nargs("*"); + subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); + subparser.addArgument("-u", "--username").help("Specify the recipient username or username link.").nargs("*"); + subparser.addArgument("--note-to-self").help("Send the pin message to self.").action(Arguments.storeTrue()); + 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("-d", "--pin-duration") + .type(int.class) + .setDefault(-1) + .help("Specify the pin duration in seconds. Use -1 for forever."); + subparser.addArgument("-a", "--target-author") + .required(true) + .help("Specify the number of the author of the message to pin."); + subparser.addArgument("-t", "--target-timestamp") + .required(true) + .type(long.class) + .help("Specify the timestamp of the message to pin."); + subparser.addArgument("--story").help("Pin 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 isNoteToSelf = Boolean.TRUE.equals(ns.getBoolean("note-to-self")); + final var recipientStrings = ns.getList("recipient"); + final var groupIdStrings = ns.getList("group-id"); + final var usernameStrings = ns.getList("username"); + + final var recipientIdentifiers = CommandUtil.getRecipientIdentifiers(m, + isNoteToSelf, + recipientStrings, + groupIdStrings, + usernameStrings); + + final var pinDuration = ns.getInt("pin-duration"); + 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; + if (targetAuthor == null && recipientIdentifiers.size() == 1 && recipientIdentifiers.stream() + .findFirst() + .get() instanceof RecipientIdentifier.Single single) { + targetAuthorIdentifier = single; + } else { + targetAuthorIdentifier = CommandUtil.getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()); + } + try { + final var results = m.sendPinMessage(pinDuration, + targetAuthorIdentifier, + targetTimestamp, + recipientIdentifiers, + 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/commands/SendUnpinMessageCommand.java b/src/main/java/org/asamk/signal/commands/SendUnpinMessageCommand.java new file mode 100644 index 00000000..067941bd --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SendUnpinMessageCommand.java @@ -0,0 +1,100 @@ +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 static org.asamk.signal.util.SendMessageResultUtils.outputResult; + +public class SendUnpinMessageCommand implements JsonRpcLocalCommand { + + @Override + public String getName() { + return "sendUnpinMessage"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help("Send unpin message for a previously received or sent message."); + subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID.").nargs("*"); + subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); + subparser.addArgument("-u", "--username").help("Specify the recipient username or username link.").nargs("*"); + subparser.addArgument("--note-to-self").help("Send the unpin message to self.").action(Arguments.storeTrue()); + 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 unpin."); + subparser.addArgument("-t", "--target-timestamp") + .required(true) + .type(long.class) + .help("Specify the timestamp of the message to unpin."); + subparser.addArgument("--story") + .help("Unpin 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 isNoteToSelf = Boolean.TRUE.equals(ns.getBoolean("note-to-self")); + final var recipientStrings = ns.getList("recipient"); + final var groupIdStrings = ns.getList("group-id"); + final var usernameStrings = ns.getList("username"); + + final var recipientIdentifiers = CommandUtil.getRecipientIdentifiers(m, + isNoteToSelf, + recipientStrings, + groupIdStrings, + usernameStrings); + + 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; + if (targetAuthor == null && recipientIdentifiers.size() == 1 && recipientIdentifiers.stream() + .findFirst() + .get() instanceof RecipientIdentifier.Single single) { + targetAuthorIdentifier = single; + } else { + targetAuthorIdentifier = CommandUtil.getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()); + } + try { + final var results = m.sendUnpinMessage(targetAuthorIdentifier, + targetTimestamp, + recipientIdentifiers, + 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 b0034b6b..d9018bfd 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -493,6 +493,29 @@ public class DbusManagerImpl implements Manager { groupId)); } + @Override + public SendMessageResults sendPinMessage( + final int pinDuration, + 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 sendUnpinMessage( + 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 sendPaymentNotificationMessage( final byte[] receipt,