From 404063a080a1a8784a1111304cc6b90b58362923 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 3 Jan 2022 13:24:13 +0100 Subject: [PATCH] Add support for sending stickers --- graalvm-config-dir/reflect-config.json | 2 + .../org/asamk/signal/manager/Manager.java | 3 +- .../org/asamk/signal/manager/ManagerImpl.java | 30 ++++++++++++- .../signal/manager/StickerPackStore.java | 21 +++++++++- .../manager/api/InvalidStickerException.java | 12 ++++++ .../org/asamk/signal/manager/api/Message.java | 10 ++++- .../helper/IncomingMessageHandler.java | 4 +- .../storage/stickers/StickerStore.java | 2 +- man/signal-cli.1.adoc | 23 +++++----- .../asamk/signal/commands/SendCommand.java | 42 +++++++++++++++---- .../org/asamk/signal/dbus/DbusSignalImpl.java | 27 ++++++++---- 11 files changed, 141 insertions(+), 35 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/InvalidStickerException.java diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index adc1da2e..2e5295cc 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -730,6 +730,7 @@ "queryAllDeclaredMethods":true, "queryAllDeclaredConstructors":true, "methods":[ + {"name":"","parameterTypes":["java.lang.String","java.lang.String","org.asamk.signal.manager.JsonStickerPack$JsonSticker","java.util.List"] }, {"name":"author","parameterTypes":[] }, {"name":"cover","parameterTypes":[] }, {"name":"stickers","parameterTypes":[] }, @@ -742,6 +743,7 @@ "queryAllDeclaredMethods":true, "queryAllDeclaredConstructors":true, "methods":[ + {"name":"","parameterTypes":["java.lang.String","java.lang.String","java.lang.String"] }, {"name":"contentType","parameterTypes":[] }, {"name":"emoji","parameterTypes":[] }, {"name":"file","parameterTypes":[] } 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 bc26d87c..a60afc8c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -6,6 +6,7 @@ import org.asamk.signal.manager.api.Group; import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.InactiveGroupLinkException; import org.asamk.signal.manager.api.InvalidDeviceLinkException; +import org.asamk.signal.manager.api.InvalidStickerException; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.MessageEnvelope; import org.asamk.signal.manager.api.Pair; @@ -173,7 +174,7 @@ public interface Manager extends Closeable { SendMessageResults sendMessage( Message message, Set recipients - ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException; + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException; SendMessageResults sendRemoteDeleteMessage( long targetSentTimestamp, Set recipients diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 5d091d29..708b0226 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -22,6 +22,7 @@ import org.asamk.signal.manager.api.Group; import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.InactiveGroupLinkException; import org.asamk.signal.manager.api.InvalidDeviceLinkException; +import org.asamk.signal.manager.api.InvalidStickerException; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -48,6 +49,7 @@ import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.stickers.Sticker; import org.asamk.signal.manager.storage.stickers.StickerPackId; +import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.StickerUtils; import org.slf4j.Logger; @@ -484,7 +486,7 @@ public class ManagerImpl implements Manager { @Override public SendMessageResults sendMessage( Message message, Set recipients - ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException { + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException { final var messageBuilder = SignalServiceDataMessage.newBuilder(); applyMessage(messageBuilder, message); return sendMessage(messageBuilder, recipients); @@ -492,7 +494,7 @@ public class ManagerImpl implements Manager { private void applyMessage( final SignalServiceDataMessage.Builder messageBuilder, final Message message - ) throws AttachmentInvalidException, IOException, UnregisteredRecipientException { + ) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException { messageBuilder.withBody(message.messageText()); final var attachments = message.attachments(); if (attachments != null) { @@ -510,6 +512,30 @@ public class ManagerImpl implements Manager { List.of(), resolveMentions(quote.mentions()))); } + if (message.sticker().isPresent()) { + final var sticker = message.sticker().get(); + final var packId = StickerPackId.deserialize(sticker.packId()); + final var stickerId = sticker.stickerId(); + + final var stickerPack = context.getAccount().getStickerStore().getStickerPack(packId); + if (stickerPack == null || !context.getStickerPackStore().existsStickerPack(packId)) { + throw new InvalidStickerException("Sticker pack not found"); + } + final var manifest = context.getStickerPackStore().retrieveManifest(packId); + if (manifest.stickers().size() <= stickerId) { + throw new InvalidStickerException("Sticker id not part of this pack"); + } + final var manifestSticker = manifest.stickers().get(stickerId); + final var streamDetails = context.getStickerPackStore().retrieveSticker(packId, stickerId); + if (streamDetails == null) { + throw new InvalidStickerException("Missing local sticker file"); + } + messageBuilder.withSticker(new SignalServiceDataMessage.Sticker(packId.serialize(), + stickerPack.getPackKey(), + stickerId, + manifestSticker.emoji(), + AttachmentUtils.createAttachment(streamDetails, Optional.absent()))); + } } private ArrayList resolveMentions(final List mentionList) throws IOException, UnregisteredRecipientException { diff --git a/lib/src/main/java/org/asamk/signal/manager/StickerPackStore.java b/lib/src/main/java/org/asamk/signal/manager/StickerPackStore.java index 51ebece3..778e748a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/StickerPackStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/StickerPackStore.java @@ -4,10 +4,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.asamk.signal.manager.storage.stickers.StickerPackId; import org.asamk.signal.manager.util.IOUtils; +import org.asamk.signal.manager.util.Utils; +import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.internal.util.Hex; import java.io.BufferedWriter; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -26,8 +29,22 @@ public class StickerPackStore { return getStickerPackManifestFile(stickerPackId).exists(); } + public JsonStickerPack retrieveManifest(StickerPackId stickerPackId) throws IOException { + try (final var inputStream = new FileInputStream(getStickerPackManifestFile(stickerPackId))) { + return new ObjectMapper().readValue(inputStream, JsonStickerPack.class); + } + } + + public StreamDetails retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException { + final var stickerFile = getStickerPackStickerFile(stickerPackId, stickerId); + if (!stickerFile.exists()) { + return null; + } + return Utils.createStreamDetailsFromFile(stickerFile); + } + public void storeManifest(StickerPackId stickerPackId, JsonStickerPack manifest) throws IOException { - try (OutputStream output = new FileOutputStream(getStickerPackManifestFile(stickerPackId))) { + try (final var output = new FileOutputStream(getStickerPackManifestFile(stickerPackId))) { try (var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) { new ObjectMapper().writeValue(writer, manifest); } @@ -36,7 +53,7 @@ public class StickerPackStore { public void storeSticker(StickerPackId stickerPackId, int stickerId, StickerStorer storer) throws IOException { createStickerPackDir(stickerPackId); - try (OutputStream output = new FileOutputStream(getStickerPackStickerFile(stickerPackId, stickerId))) { + try (final var output = new FileOutputStream(getStickerPackStickerFile(stickerPackId, stickerId))) { storer.store(output); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/InvalidStickerException.java b/lib/src/main/java/org/asamk/signal/manager/api/InvalidStickerException.java new file mode 100644 index 00000000..2e2ffd4b --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/InvalidStickerException.java @@ -0,0 +1,12 @@ +package org.asamk.signal.manager.api; + +public class InvalidStickerException extends Exception { + + public InvalidStickerException(final String message) { + super(message); + } + + public InvalidStickerException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Message.java b/lib/src/main/java/org/asamk/signal/manager/api/Message.java index 13cb00cf..3df110cd 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Message.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Message.java @@ -3,9 +3,17 @@ package org.asamk.signal.manager.api; import java.util.List; import java.util.Optional; -public record Message(String messageText, List attachments, List mentions, Optional quote) { +public record Message( + String messageText, + List attachments, + List mentions, + Optional quote, + Optional sticker +) { public record Mention(RecipientIdentifier.Single recipient, int start, int length) {} public record Quote(long timestamp, RecipientIdentifier.Single author, String message, List mentions) {} + + public record Sticker(byte[] packId, int stickerId) {} } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index d5ada09e..2efcdcaa 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -327,7 +327,7 @@ public final class IncomingMessageHandler { final var installed = !m.getType().isPresent() || m.getType().get() == StickerPackOperationMessage.Type.INSTALL; - var sticker = account.getStickerStore().getSticker(stickerPackId); + var sticker = account.getStickerStore().getStickerPack(stickerPackId); if (m.getPackKey().isPresent()) { if (sticker == null) { sticker = new Sticker(stickerPackId, m.getPackKey().get()); @@ -598,7 +598,7 @@ public final class IncomingMessageHandler { if (message.getSticker().isPresent()) { final var messageSticker = message.getSticker().get(); final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId()); - var sticker = account.getStickerStore().getSticker(stickerPackId); + var sticker = account.getStickerStore().getStickerPack(stickerPackId); if (sticker == null) { sticker = new Sticker(stickerPackId, messageSticker.getPackKey()); account.getStickerStore().updateSticker(sticker); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/stickers/StickerStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/stickers/StickerStore.java index 4b045ac1..1a67c15a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/stickers/StickerStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/stickers/StickerStore.java @@ -41,7 +41,7 @@ public class StickerStore { return new StickerStore(stickers, saver); } - public Sticker getSticker(StickerPackId packId) { + public Sticker getStickerPack(StickerPackId packId) { synchronized (stickers) { return stickers.get(packId); } diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 57016413..2104ffc5 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -73,11 +73,9 @@ Choose when to trust new identities: Register a phone number with SMS or voice verification. Use the verify command to complete the verification. -If the account is just deactivated, the register command will just reactivate -account, without requiring an SMS verification. By default the unregister command -just deactivates the account, in which case it can be reactivated without sms -verification if the local data is still available. If the account was deleted -(with --delete-account) it cannot be reactivated. +If the account is just deactivated, the register command will just reactivate account, without requiring an SMS verification. +By default the unregister command just deactivates the account, in which case it can be reactivated without sms verification if the local data is still available. +If the account was deleted (with --delete-account) it cannot be reactivated. *-v*, *--voice*:: The verification should be done over voice, not SMS. @@ -197,6 +195,9 @@ Send a message to another user or group. RECIPIENT:: Specify the recipients’ phone number. +*--note-to-self*:: +Send the message to self without notification. + *-g* GROUP, *--group-id* GROUP:: Specify the recipient group ID in base64 encoding. @@ -206,11 +207,10 @@ Specify the message, if missing, standard input is used. *-a* [ATTACHMENT [ATTACHMENT ...]], *--attachment* [ATTACHMENT [ATTACHMENT ...]]:: Add one or more files as attachment. -*--note-to-self*:: -Send the message to self without notification. - -*-e*, *--end-session*:: -Clear session state and send end session message. +*--sticker* STICKER:: +Send a sticker of a locally known sticker pack (syntax: stickerPackId:stickerId). +Shouldn't be used together with `-m` as the official clients don't support this. +e.g.: `--sticker 00abac3bc18d7f599bff2325dc306d43:2` *--mention*:: Mention another group member (syntax: start:length:recipientNumber) In the apps the mention replaces part of the message text, which is specified by the start and length values. @@ -228,6 +228,9 @@ Specify the message of the original message. *--quote-mention*:: Specify the mentions of the original message (same format as `--mention`). +*-e*, *--end-session*:: +Clear session state and send end session message. + === sendReaction Send reaction to a previously received or sent message. diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 4d3f80aa..74209feb 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -9,6 +9,7 @@ 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.api.InvalidStickerException; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.UnregisteredRecipientException; @@ -17,6 +18,7 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.output.OutputWriter; import org.asamk.signal.util.CommandUtil; +import org.asamk.signal.util.Hex; import org.asamk.signal.util.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,6 +67,7 @@ public class SendCommand implements JsonRpcLocalCommand { subparser.addArgument("--quote-mention") .nargs("*") .help("Quote with mention of another group member (syntax: start:length:recipientNumber)"); + subparser.addArgument("--sticker").help("Send a sticker (syntax: stickerPackId:stickerId)"); } @Override @@ -100,13 +103,20 @@ public class SendCommand implements JsonRpcLocalCommand { } } + final var stickerString = ns.getString("sticker"); + final var sticker = stickerString == null ? null : parseSticker(stickerString); + var messageText = ns.getString("message"); if (messageText == null) { - logger.debug("Reading message from stdin..."); - try { - messageText = IOUtils.readAll(System.in, Charset.defaultCharset()); - } catch (IOException e) { - throw new UserErrorException("Failed to read message from stdin: " + e.getMessage()); + if (sticker != null) { + messageText = ""; + } else { + logger.debug("Reading message from stdin..."); + try { + messageText = IOUtils.readAll(System.in, Charset.defaultCharset()); + } catch (IOException e) { + throw new UserErrorException("Failed to read message from stdin: " + e.getMessage()); + } } } @@ -129,15 +139,18 @@ public class SendCommand implements JsonRpcLocalCommand { : parseMentions(m, quoteMentionStrings); quote = new Message.Quote(quoteTimestamp, CommandUtil.getSingleRecipientIdentifier(quoteAuthor, m.getSelfNumber()), - quoteMessage, + quoteMessage == null ? "" : quoteMessage, quoteMentions); } else { quote = null; } try { - var results = m.sendMessage(new Message(messageText, attachments, mentions, Optional.ofNullable(quote)), - recipientIdentifiers); + var results = m.sendMessage(new Message(messageText, + attachments, + mentions, + Optional.ofNullable(quote), + Optional.ofNullable(sticker)), recipientIdentifiers); outputResult(outputWriter, results); } catch (AttachmentInvalidException | IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() @@ -146,6 +159,8 @@ public class SendCommand implements JsonRpcLocalCommand { throw new UserErrorException(e.getMessage()); } catch (UnregisteredRecipientException e) { throw new UserErrorException("The user " + e.getSender().getIdentifier() + " is not registered."); + } catch (InvalidStickerException e) { + throw new UserErrorException("Failed to send sticker: " + e.getMessage(), e); } } @@ -167,4 +182,15 @@ public class SendCommand implements JsonRpcLocalCommand { } return mentions; } + + private Message.Sticker parseSticker(final String stickerString) throws UserErrorException { + final Pattern stickerPattern = Pattern.compile("([0-9a-f]+):([0-9]+)"); + final var matcher = stickerPattern.matcher(stickerString); + if (!matcher.matches() || matcher.group(1).length() % 2 != 0) { + throw new UserErrorException("Invalid sticker syntax (" + + stickerString + + ") expected 'stickerPackId:stickerId'"); + } + return new Message.Sticker(Hex.toByteArray(matcher.group(1)), Integer.parseInt(matcher.group(2))); + } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 003f6695..68218947 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -10,6 +10,7 @@ import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.InactiveGroupLinkException; import org.asamk.signal.manager.api.InvalidDeviceLinkException; import org.asamk.signal.manager.api.InvalidNumberException; +import org.asamk.signal.manager.api.InvalidStickerException; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -199,7 +200,11 @@ public class DbusSignalImpl implements Signal { @Override public long sendMessage(final String message, final List attachments, final List recipients) { try { - final var results = m.sendMessage(new Message(message, attachments, List.of(), Optional.empty()), + final var results = m.sendMessage(new Message(message, + attachments, + List.of(), + Optional.empty(), + Optional.empty()), getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); @@ -208,7 +213,7 @@ public class DbusSignalImpl implements Signal { return results.timestamp(); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); - } catch (IOException e) { + } catch (IOException | InvalidStickerException e) { throw new Error.Failure(e); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); @@ -346,13 +351,16 @@ public class DbusSignalImpl implements Signal { final String message, final List attachments ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity { try { - final var results = m.sendMessage(new Message(message, attachments, List.of(), Optional.empty()), - Set.of(RecipientIdentifier.NoteToSelf.INSTANCE)); + final var results = m.sendMessage(new Message(message, + attachments, + List.of(), + Optional.empty(), + Optional.empty()), Set.of(RecipientIdentifier.NoteToSelf.INSTANCE)); checkSendMessageResults(results); return results.timestamp(); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); - } catch (IOException e) { + } catch (IOException | InvalidStickerException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); @@ -384,11 +392,14 @@ public class DbusSignalImpl implements Signal { @Override public long sendGroupMessage(final String message, final List attachments, final byte[] groupId) { try { - var results = m.sendMessage(new Message(message, attachments, List.of(), Optional.empty()), - Set.of(getGroupRecipientIdentifier(groupId))); + var results = m.sendMessage(new Message(message, + attachments, + List.of(), + Optional.empty(), + Optional.empty()), Set.of(getGroupRecipientIdentifier(groupId))); checkSendMessageResults(results); return results.timestamp(); - } catch (IOException e) { + } catch (IOException | InvalidStickerException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); -- 2.50.1