From feaee2bfe127eff4c7fabe2604a5d3a50e4989e6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 2 Nov 2025 18:24:47 +0100 Subject: [PATCH] Add support for receiving and sending polls --- graalvm-config-dir/reflect-config.json | 23 +++- .../org/asamk/signal/manager/Manager.java | 23 ++++ .../signal/manager/api/MessageEnvelope.java | 44 +++++++ .../signal/manager/internal/ManagerImpl.java | 45 ++++++++ .../asamk/signal/ReceiveMessageHandler.java | 21 ++++ .../org/asamk/signal/commands/Commands.java | 3 + .../commands/SendPollCreateCommand.java | 94 +++++++++++++++ .../commands/SendPollTerminateCommand.java | 88 ++++++++++++++ .../signal/commands/SendPollVoteCommand.java | 107 ++++++++++++++++++ .../asamk/signal/dbus/DbusManagerImpl.java | 41 +++++++ .../asamk/signal/json/JsonDataMessage.java | 9 ++ .../org/asamk/signal/json/JsonPollCreate.java | 18 +++ .../asamk/signal/json/JsonPollTerminate.java | 12 ++ .../org/asamk/signal/json/JsonPollVote.java | 28 +++++ 14 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/asamk/signal/commands/SendPollCreateCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/SendPollTerminateCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/SendPollVoteCommand.java create mode 100644 src/main/java/org/asamk/signal/json/JsonPollCreate.java create mode 100644 src/main/java/org/asamk/signal/json/JsonPollTerminate.java create mode 100644 src/main/java/org/asamk/signal/json/JsonPollVote.java diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index c414fb34..32dcbf16 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -1093,7 +1093,7 @@ "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true, - "methods":[{"name":"attachments","parameterTypes":[] }, {"name":"contacts","parameterTypes":[] }, {"name":"expiresInSeconds","parameterTypes":[] }, {"name":"groupInfo","parameterTypes":[] }, {"name":"isExpirationUpdate","parameterTypes":[] }, {"name":"mentions","parameterTypes":[] }, {"name":"message","parameterTypes":[] }, {"name":"payment","parameterTypes":[] }, {"name":"previews","parameterTypes":[] }, {"name":"quote","parameterTypes":[] }, {"name":"reaction","parameterTypes":[] }, {"name":"remoteDelete","parameterTypes":[] }, {"name":"sticker","parameterTypes":[] }, {"name":"storyContext","parameterTypes":[] }, {"name":"textStyles","parameterTypes":[] }, {"name":"timestamp","parameterTypes":[] }, {"name":"viewOnce","parameterTypes":[] }] + "methods":[{"name":"attachments","parameterTypes":[] }, {"name":"contacts","parameterTypes":[] }, {"name":"expiresInSeconds","parameterTypes":[] }, {"name":"groupInfo","parameterTypes":[] }, {"name":"isExpirationUpdate","parameterTypes":[] }, {"name":"mentions","parameterTypes":[] }, {"name":"message","parameterTypes":[] }, {"name":"payment","parameterTypes":[] }, {"name":"pollCreate","parameterTypes":[] }, {"name":"pollTerminate","parameterTypes":[] }, {"name":"pollVote","parameterTypes":[] }, {"name":"previews","parameterTypes":[] }, {"name":"quote","parameterTypes":[] }, {"name":"reaction","parameterTypes":[] }, {"name":"remoteDelete","parameterTypes":[] }, {"name":"sticker","parameterTypes":[] }, {"name":"storyContext","parameterTypes":[] }, {"name":"textStyles","parameterTypes":[] }, {"name":"timestamp","parameterTypes":[] }, {"name":"viewOnce","parameterTypes":[] }] }, { "name":"org.asamk.signal.json.JsonEditMessage", @@ -1137,6 +1137,27 @@ "queryAllDeclaredConstructors":true, "methods":[{"name":"note","parameterTypes":[] }, {"name":"receipt","parameterTypes":[] }] }, +{ + "name":"org.asamk.signal.json.JsonPollCreate", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"allowMultiple","parameterTypes":[] }, {"name":"options","parameterTypes":[] }, {"name":"question","parameterTypes":[] }] +}, +{ + "name":"org.asamk.signal.json.JsonPollTerminate", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"targetSentTimestamp","parameterTypes":[] }] +}, +{ + "name":"org.asamk.signal.json.JsonPollVote", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"author","parameterTypes":[] }, {"name":"authorNumber","parameterTypes":[] }, {"name":"authorUuid","parameterTypes":[] }, {"name":"optionIndexes","parameterTypes":[] }, {"name":"targetSentTimestamp","parameterTypes":[] }, {"name":"voteCount","parameterTypes":[] }] +}, { "name":"org.asamk.signal.json.JsonPreview", "allDeclaredFields":true, 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 f21516ea..4f2e152a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -235,6 +235,29 @@ public interface Manager extends Closeable { Set recipientIdentifiers ); + SendMessageResults sendPollCreateMessage( + final String question, + final boolean allowMultiple, + final List options, + final Set recipients, + final boolean notifySelf + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException; + + SendMessageResults sendPollVoteMessage( + final RecipientIdentifier.Single targetAuthor, + final long targetSentTimestamp, + final List optionIndexes, + final int voteCount, + final Set recipients, + final boolean notifySelf + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException; + + SendMessageResults sendPollTerminateMessage( + final long targetSentTimestamp, + final Set recipients, + final boolean notifySelf + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException; + void hideRecipient(RecipientIdentifier.Single recipient); void deleteRecipient(RecipientIdentifier.Single recipient); diff --git a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java index 1d438c30..d7832e61 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java @@ -115,6 +115,9 @@ public record MessageEnvelope( Optional remoteDeleteId, Optional sticker, List sharedContacts, + Optional pollCreate, + Optional pollVote, + Optional pollTerminate, List mentions, List previews, List textStyles @@ -155,6 +158,9 @@ public record MessageEnvelope( .map(sharedContact -> SharedContact.from(sharedContact, fileProvider)) .toList()) .orElse(List.of()), + dataMessage.getPollCreate().map(PollCreate::from), + dataMessage.getPollVote().map(p -> PollVote.from(p, recipientResolver, addressResolver)), + dataMessage.getPollTerminate().map(PollTerminate::from), dataMessage.getMentions() .map(a -> a.stream().map(m -> Mention.from(m, recipientResolver, addressResolver)).toList()) .orElse(List.of()), @@ -509,6 +515,44 @@ public record MessageEnvelope( } } + public record PollCreate( + String question, boolean allowMultiple, List options + ) { + + static PollCreate from( + SignalServiceDataMessage.PollCreate pollCreate + ) { + return new PollCreate(pollCreate.getQuestion(), pollCreate.getAllowMultiple(), pollCreate.getOptions()); + } + + } + + public record PollVote( + RecipientAddress targetAuthor, long targetSentTimestamp, List optionIndexes, int voteCount + ) { + + static PollVote from( + SignalServiceDataMessage.PollVote pollVote, + RecipientResolver recipientResolver, + RecipientAddressResolver addressResolver + ) { + return new PollVote(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(pollVote.getTargetAuthor())) + .toApiRecipientAddress(), + pollVote.getTargetSentTimestamp(), + pollVote.getOptionIndexes(), + pollVote.getVoteCount()); + } + + } + + public record PollTerminate(long targetSentTimestamp) { + + static PollTerminate from(SignalServiceDataMessage.PollTerminate pollTerminate) { + return new PollTerminate(pollTerminate.getTargetSentTimestamp()); + } + + } + public record Preview(String title, String description, long date, String url, Optional image) { static Preview from(SignalServicePreview preview, final AttachmentFileProvider fileProvider) { 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 5dabde36..a768813a 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 @@ -1058,6 +1058,51 @@ public class ManagerImpl implements Manager { return new SendMessageResults(0, results); } + @Override + public SendMessageResults sendPollCreateMessage( + final String question, + final boolean allowMultiple, + final List options, + final Set recipients, + final boolean notifySelf + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException { + final var pollCreate = new SignalServiceDataMessage.PollCreate(question, allowMultiple, options); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withPollCreate(pollCreate); + return sendMessage(messageBuilder, recipients, notifySelf); + } + + @Override + public SendMessageResults sendPollVoteMessage( + final RecipientIdentifier.Single targetAuthor, + final long targetSentTimestamp, + final List optionIndexes, + final int voteCount, + final Set recipients, + final boolean notifySelf + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException { + final var targetAuthorRecipientId = context.getRecipientHelper().resolveRecipient(targetAuthor); + final var authorServiceId = context.getRecipientHelper() + .resolveSignalServiceAddress(targetAuthorRecipientId) + .getServiceId(); + final var pollVote = new SignalServiceDataMessage.PollVote(authorServiceId, + targetSentTimestamp, + optionIndexes, + voteCount); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withPollVote(pollVote); + return sendMessage(messageBuilder, recipients, notifySelf); + } + + @Override + public SendMessageResults sendPollTerminateMessage( + final long targetSentTimestamp, + final Set recipients, + final boolean notifySelf + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException { + final var pollTerminate = new SignalServiceDataMessage.PollTerminate(targetSentTimestamp); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withPollTerminate(pollTerminate); + return sendMessage(messageBuilder, recipients, notifySelf); + } + @Override public void hideRecipient(final RecipientIdentifier.Single recipient) { final var recipientIdOptional = context.getRecipientHelper().resolveRecipientOptional(recipient); diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index d7fd1104..c282cce8 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -193,6 +193,27 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { printAttachment(writer.indentedWriter(), attachment); } } + if (!message.pollCreate().isEmpty()) { + final var pollCreate = message.pollCreate().get(); + writer.println("Poll Create: \"{}\" ({})", + pollCreate.question(), + pollCreate.allowMultiple() ? "multi" : "single"); + for (final var option : pollCreate.options()) { + writer.println("- {}", option); + } + } + if (!message.pollVote().isEmpty()) { + final var pollVote = message.pollVote().get(); + writer.println("Poll Vote: \"{}\" ({}) selected {} (vote #{})", + formatContact(pollVote.targetAuthor()), + DateUtils.formatTimestamp(pollVote.targetSentTimestamp()), + pollVote.optionIndexes().stream().map(Object::toString).collect(Collectors.joining(",")), + pollVote.voteCount()); + } + if (!message.pollTerminate().isEmpty()) { + final var pollTerminate = message.pollTerminate().get(); + writer.println("Poll Terminate: {}", DateUtils.formatTimestamp(pollTerminate.targetSentTimestamp())); + } } private void printEditMessage(PlainTextWriter writer, MessageEnvelope.Edit message) { diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 51620a7f..52d48077 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -41,6 +41,9 @@ public class Commands { addCommand(new SendContactsCommand()); addCommand(new SendMessageRequestResponseCommand()); addCommand(new SendPaymentNotificationCommand()); + addCommand(new SendPollCreateCommand()); + addCommand(new SendPollVoteCommand()); + addCommand(new SendPollTerminateCommand()); addCommand(new SendReactionCommand()); addCommand(new SendReceiptCommand()); addCommand(new SendSyncRequestCommand()); diff --git a/src/main/java/org/asamk/signal/commands/SendPollCreateCommand.java b/src/main/java/org/asamk/signal/commands/SendPollCreateCommand.java new file mode 100644 index 00000000..7780e11c --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SendPollCreateCommand.java @@ -0,0 +1,94 @@ +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.UnregisteredRecipientException; +import org.asamk.signal.output.OutputWriter; +import org.asamk.signal.util.CommandUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import static org.asamk.signal.util.SendMessageResultUtils.outputResult; + +public class SendPollCreateCommand implements JsonRpcLocalCommand { + + private static final Logger logger = LoggerFactory.getLogger(SendPollCreateCommand.class); + + @Override + public String getName() { + return "sendPollCreate"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help("Create a poll and send it to another user or group."); + subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); + subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID.").nargs("*"); + subparser.addArgument("-u", "--username").help("Specify the recipient username or username link.").nargs("*"); + subparser.addArgument("--note-to-self").help("Send the 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("-q", "--question").help("Specify the poll question.").required(true); + subparser.addArgument("--no-multi") + .action(Arguments.storeTrue()) + .help("Allow only one option to be selected by each recipient."); + subparser.addArgument("-o", "--option").help("The options for the poll").nargs("+").required(true); + } + + @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 question = ns.getString("question"); + final var noMulti = Boolean.TRUE.equals(ns.getBoolean("no-multi")); + final var options = ns.getList("option"); + if (options.size() < 2) { + throw new UserErrorException("Poll needs at least tow options"); + } + + try { + var results = m.sendPollCreateMessage(question, !noMulti, options, recipientIdentifiers, notifySelf); + outputResult(outputWriter, results); + } catch (IOException e) { + if (e.getMessage().contains("No prekeys available")) { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + "), maybe one of the devices of the recipient wasn't online for a while.", + e); + } else { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")", e); + } + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new UserErrorException(e.getMessage()); + } catch (UnregisteredRecipientException e) { + throw new UserErrorException("The user " + e.getSender().getIdentifier() + " is not registered."); + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/SendPollTerminateCommand.java b/src/main/java/org/asamk/signal/commands/SendPollTerminateCommand.java new file mode 100644 index 00000000..9b4e6b8a --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SendPollTerminateCommand.java @@ -0,0 +1,88 @@ +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.UnregisteredRecipientException; +import org.asamk.signal.output.OutputWriter; +import org.asamk.signal.util.CommandUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import static org.asamk.signal.util.SendMessageResultUtils.outputResult; + +public class SendPollTerminateCommand implements JsonRpcLocalCommand { + + private static final Logger logger = LoggerFactory.getLogger(SendPollTerminateCommand.class); + + @Override + public String getName() { + return "sendPollTerminate"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help("Terminate a poll and send it to another user or group."); + subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); + subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID.").nargs("*"); + subparser.addArgument("-u", "--username").help("Specify the recipient username or username link.").nargs("*"); + subparser.addArgument("--note-to-self").help("Send the 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("--poll-timestamp") + .type(long.class) + .help("Specify the timestamp of the original poll message.") + .required(true); + } + + @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 pollTimestamp = ns.getLong("poll-timestamp"); + + try { + var results = m.sendPollTerminateMessage(pollTimestamp, recipientIdentifiers, notifySelf); + outputResult(outputWriter, results); + } catch (IOException e) { + if (e.getMessage().contains("No prekeys available")) { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + "), maybe one of the devices of the recipient wasn't online for a while.", + e); + } else { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")", e); + } + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new UserErrorException(e.getMessage()); + } catch (UnregisteredRecipientException e) { + throw new UserErrorException("The user " + e.getSender().getIdentifier() + " is not registered."); + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/SendPollVoteCommand.java b/src/main/java/org/asamk/signal/commands/SendPollVoteCommand.java new file mode 100644 index 00000000..4b36da2e --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SendPollVoteCommand.java @@ -0,0 +1,107 @@ +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.UnregisteredRecipientException; +import org.asamk.signal.output.OutputWriter; +import org.asamk.signal.util.CommandUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import static org.asamk.signal.util.SendMessageResultUtils.outputResult; + +public class SendPollVoteCommand implements JsonRpcLocalCommand { + + private static final Logger logger = LoggerFactory.getLogger(SendPollVoteCommand.class); + + @Override + public String getName() { + return "sendPollVote"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help("Vote on a poll and send it to another user or group."); + subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); + subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID.").nargs("*"); + subparser.addArgument("-u", "--username").help("Specify the recipient username or username link.").nargs("*"); + subparser.addArgument("--note-to-self").help("Send the 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("--poll-author").help("Specify the number of the author of the poll message."); + subparser.addArgument("--poll-timestamp") + .type(long.class) + .help("Specify the timestamp of the original poll message.") + .required(true); + subparser.addArgument("-o", "--option") + .type(Integer.class) + .help("The option indexes of the poll to vote for") + .nargs("*"); + subparser.addArgument("--vote-count") + .type(int.class) + .help("Specify the number of this vote (increase by one for every time you vote).") + .required(true); + } + + @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 selfNumber = m.getSelfNumber(); + final var pollTimestamp = ns.getLong("poll-timestamp"); + final var pollAuthorString = ns.getString("poll-author"); + final var pollAuthor = CommandUtil.getSingleRecipientIdentifier(pollAuthorString, selfNumber); + final var options = ns.getList("option"); + final var voteCount = ns.getInt("vote-count"); + + try { + var results = m.sendPollVoteMessage(pollAuthor, + pollTimestamp, + options, + voteCount, + recipientIdentifiers, + notifySelf); + outputResult(outputWriter, results); + } catch (IOException e) { + if (e.getMessage().contains("No prekeys available")) { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + "), maybe one of the devices of the recipient wasn't online for a while.", + e); + } else { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")", e); + } + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new UserErrorException(e.getMessage()); + } 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 0d64b88b..6da1ab2b 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -509,6 +509,38 @@ public class DbusManagerImpl implements Manager { throw new UnsupportedOperationException(); } + @Override + public SendMessageResults sendPollCreateMessage( + final String question, + final boolean allowMultiple, + final List options, + final Set recipients, + final boolean notifySelf + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException { + throw new UnsupportedOperationException(); + } + + @Override + public SendMessageResults sendPollVoteMessage( + final RecipientIdentifier.Single targetAuthor, + final long targetSentTimestamp, + final List optionIndexes, + final int voteCount, + final Set recipients, + final boolean notifySelf + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException { + throw new UnsupportedOperationException(); + } + + @Override + public SendMessageResults sendPollTerminateMessage( + final long targetSentTimestamp, + final Set recipients, + final boolean notifySelf + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException { + throw new UnsupportedOperationException(); + } + public void hideRecipient(final RecipientIdentifier.Single recipient) { throw new UnsupportedOperationException(); } @@ -932,6 +964,9 @@ public class DbusManagerImpl implements Manager { Optional.empty(), Optional.empty(), List.of(), + Optional.empty(), + Optional.empty(), + Optional.empty(), getMentions(extras), List.of(), List.of())), @@ -975,6 +1010,9 @@ public class DbusManagerImpl implements Manager { Optional.empty(), Optional.empty(), List.of(), + Optional.empty(), + Optional.empty(), + Optional.empty(), getMentions(extras), List.of(), List.of()))), @@ -1050,6 +1088,9 @@ public class DbusManagerImpl implements Manager { Optional.empty(), Optional.empty(), List.of(), + Optional.empty(), + Optional.empty(), + Optional.empty(), getMentions(extras), List.of(), List.of())), diff --git a/src/main/java/org/asamk/signal/json/JsonDataMessage.java b/src/main/java/org/asamk/signal/json/JsonDataMessage.java index bf3b437b..b141265e 100644 --- a/src/main/java/org/asamk/signal/json/JsonDataMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonDataMessage.java @@ -22,6 +22,9 @@ record JsonDataMessage( @JsonInclude(JsonInclude.Include.NON_NULL) JsonSticker sticker, @JsonInclude(JsonInclude.Include.NON_NULL) JsonRemoteDelete remoteDelete, @JsonInclude(JsonInclude.Include.NON_NULL) List contacts, + @JsonInclude(JsonInclude.Include.NON_NULL) JsonPollCreate pollCreate, + @JsonInclude(JsonInclude.Include.NON_NULL) JsonPollVote pollVote, + @JsonInclude(JsonInclude.Include.NON_NULL) JsonPollTerminate pollTerminate, @JsonInclude(JsonInclude.Include.NON_NULL) List textStyles, @JsonInclude(JsonInclude.Include.NON_NULL) JsonGroupInfo groupInfo, @JsonInclude(JsonInclude.Include.NON_NULL) JsonStoryContext storyContext @@ -61,6 +64,9 @@ record JsonDataMessage( .stream() .map(JsonSharedContact::from) .toList() : null; + final var pollCreate = dataMessage.pollCreate().map(JsonPollCreate::from).orElse(null); + final var pollVote = dataMessage.pollVote().map(JsonPollVote::from).orElse(null); + final var pollTerminate = dataMessage.pollTerminate().map(JsonPollTerminate::from).orElse(null); final var textStyles = !dataMessage.textStyles().isEmpty() ? dataMessage.textStyles() .stream() .map(JsonTextStyle::from) @@ -80,6 +86,9 @@ record JsonDataMessage( sticker, remoteDelete, contacts, + pollCreate, + pollVote, + pollTerminate, textStyles, groupInfo, storyContext); diff --git a/src/main/java/org/asamk/signal/json/JsonPollCreate.java b/src/main/java/org/asamk/signal/json/JsonPollCreate.java new file mode 100644 index 00000000..2c36c4ca --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonPollCreate.java @@ -0,0 +1,18 @@ +package org.asamk.signal.json; + +import org.asamk.signal.manager.api.MessageEnvelope; + +import java.util.List; + +public record JsonPollCreate( + String question, boolean allowMultiple, List options +) { + + static JsonPollCreate from(MessageEnvelope.Data.PollCreate pollCreate) { + final var question = pollCreate.question(); + final var allowMultiple = pollCreate.allowMultiple(); + final var options = pollCreate.options(); + + return new JsonPollCreate(question, allowMultiple, options); + } +} diff --git a/src/main/java/org/asamk/signal/json/JsonPollTerminate.java b/src/main/java/org/asamk/signal/json/JsonPollTerminate.java new file mode 100644 index 00000000..0642559f --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonPollTerminate.java @@ -0,0 +1,12 @@ +package org.asamk.signal.json; + +import org.asamk.signal.manager.api.MessageEnvelope; + +public record JsonPollTerminate(long targetSentTimestamp) { + + static JsonPollTerminate from(MessageEnvelope.Data.PollTerminate pollTerminate) { + final var targetSentTimestamp = pollTerminate.targetSentTimestamp(); + + return new JsonPollTerminate(targetSentTimestamp); + } +} diff --git a/src/main/java/org/asamk/signal/json/JsonPollVote.java b/src/main/java/org/asamk/signal/json/JsonPollVote.java new file mode 100644 index 00000000..520a838e --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonPollVote.java @@ -0,0 +1,28 @@ +package org.asamk.signal.json; + +import org.asamk.signal.manager.api.MessageEnvelope; + +import java.util.List; +import java.util.UUID; + +public record JsonPollVote( + @Deprecated String author, + String authorNumber, + String authorUuid, + long targetSentTimestamp, + List optionIndexes, + int voteCount +) { + + static JsonPollVote from(MessageEnvelope.Data.PollVote pollVote) { + final var address = pollVote.targetAuthor(); + final var author = address.getLegacyIdentifier(); + final var authorNumber = address.number().orElse(null); + final var authorUuid = address.uuid().map(UUID::toString).orElse(null); + final var targetSentTimestamp = pollVote.targetSentTimestamp(); + final var optionIndexes = pollVote.optionIndexes(); + final var voteCount = pollVote.voteCount(); + + return new JsonPollVote(author, authorNumber, authorUuid, targetSentTimestamp, optionIndexes, voteCount); + } +}