]> nmode's Git Repositories - signal-cli/commitdiff
Add support for sending stickers
authorAsamK <asamk@gmx.de>
Mon, 3 Jan 2022 12:24:13 +0000 (13:24 +0100)
committerAsamK <asamk@gmx.de>
Mon, 3 Jan 2022 12:24:13 +0000 (13:24 +0100)
graalvm-config-dir/reflect-config.json
lib/src/main/java/org/asamk/signal/manager/Manager.java
lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
lib/src/main/java/org/asamk/signal/manager/StickerPackStore.java
lib/src/main/java/org/asamk/signal/manager/api/InvalidStickerException.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/api/Message.java
lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java
lib/src/main/java/org/asamk/signal/manager/storage/stickers/StickerStore.java
man/signal-cli.1.adoc
src/main/java/org/asamk/signal/commands/SendCommand.java
src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java

index adc1da2e29433859fe7d1bc1dc003ed61949f0d7..2e5295cc9219ab0142c7f6a4023ef854918a7028 100644 (file)
   "queryAllDeclaredMethods":true,
   "queryAllDeclaredConstructors":true,
   "methods":[
+    {"name":"<init>","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":[] }, 
   "queryAllDeclaredMethods":true,
   "queryAllDeclaredConstructors":true,
   "methods":[
+    {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","java.lang.String"] }, 
     {"name":"contentType","parameterTypes":[] }, 
     {"name":"emoji","parameterTypes":[] }, 
     {"name":"file","parameterTypes":[] }
index bc26d87cc2a407df22b197fbbb87540ef4bddf1d..a60afc8cfdfd8a8cebca17f42aa67b2eeb446b50 100644 (file)
@@ -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<RecipientIdentifier> recipients
-    ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
+    ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
 
     SendMessageResults sendRemoteDeleteMessage(
             long targetSentTimestamp, Set<RecipientIdentifier> recipients
index 5d091d29631f21d52010e79597c98d91cd507250..708b0226ddd513e50d68bbdc5533de62fc8dc03f 100644 (file)
@@ -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<RecipientIdentifier> 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<SignalServiceDataMessage.Mention> resolveMentions(final List<Message.Mention> mentionList) throws IOException, UnregisteredRecipientException {
index 51ebece3ec3310e11bf78002f8f00c34f98a54c1..778e748ad6340891a02b24404c3cbdb97a547a49 100644 (file)
@@ -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 (file)
index 0000000..2e2ffd4
--- /dev/null
@@ -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);
+    }
+}
index 13cb00cf72b1b1c6737a0f10146142772904bfc4..3df110cdd8464848c26411a09be2f1e3c0a58ae4 100644 (file)
@@ -3,9 +3,17 @@ package org.asamk.signal.manager.api;
 import java.util.List;
 import java.util.Optional;
 
-public record Message(String messageText, List<String> attachments, List<Mention> mentions, Optional<Quote> quote) {
+public record Message(
+        String messageText,
+        List<String> attachments,
+        List<Mention> mentions,
+        Optional<Quote> quote,
+        Optional<Sticker> sticker
+) {
 
     public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
 
     public record Quote(long timestamp, RecipientIdentifier.Single author, String message, List<Mention> mentions) {}
+
+    public record Sticker(byte[] packId, int stickerId) {}
 }
index d5ada09ee32f4dc662a8791f42a8c79fc23dbbfa..2efcdcaa59b9f0d89318ec437687ad1e7805c1e9 100644 (file)
@@ -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);
index 4b045ac108c504b11bc5ebf6091a46c88050937c..1a67c15ad9fd5273f8e6ccaf170d9a853b295928 100644 (file)
@@ -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);
         }
index 5701641390df15d2abb435b0548d6886c5fa232e..2104ffc5771041677d645bc8887a1f610d979a38 100644 (file)
@@ -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.
index 4d3f80aad69d4bfe150a51c1bb88e2ec95f9958b..74209feb54d8cbb8ec5e74b3d0d206532445f9e5 100644 (file)
@@ -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)));
+    }
 }
index 003f6695bd1eddcd18a74e0fc763c22524c56312..68218947b57fd4e94b9a1bbe4c21b53063710d3b 100644 (file)
@@ -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<String> attachments, final List<String> 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<String> 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<String> 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());