]> nmode's Git Repositories - signal-cli/commitdiff
Add command to retrieve avatars and stickers
authorAsamK <asamk@gmx.de>
Fri, 9 Feb 2024 21:06:46 +0000 (22:06 +0100)
committerAsamK <asamk@gmx.de>
Fri, 9 Feb 2024 21:10:46 +0000 (22:10 +0100)
Fixes #1125

13 files changed:
CHANGELOG.md
client/src/cli.rs
client/src/jsonrpc.rs
client/src/main.rs
lib/src/main/java/org/asamk/signal/manager/Manager.java
lib/src/main/java/org/asamk/signal/manager/api/StickerPackId.java
lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java
man/signal-cli.1.adoc
src/main/java/org/asamk/signal/commands/Commands.java
src/main/java/org/asamk/signal/commands/GetAttachmentCommand.java
src/main/java/org/asamk/signal/commands/GetAvatarCommand.java [new file with mode: 0644]
src/main/java/org/asamk/signal/commands/GetStickerCommand.java [new file with mode: 0644]
src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java

index 399501841d74c7103cde34432f2c6e7ba948308b..fa0aff9cb353179d3943394d1881c89b2eb24a73 100644 (file)
@@ -14,6 +14,7 @@
   behavior, the `--notify-self` parameter can be added
 - New `--unrestricted-unidentified-sender` parameter for `updateAccount command`
 - New `--bus-name` parameter for `daemon` command to use another D-Bus bus name
+- New `getAvatar` and `getSticker` commands to get avatar and sticker images
 
 ### Improved
 
index dea3a7dfd03308de3eaca037e90a0edc96204812..33403890821716278606cb66ed1087e39777b9dd 100644 (file)
@@ -68,6 +68,20 @@ pub enum CliCommands {
         #[arg(short = 'g', long = "group-id")]
         group_id: Option<String>,
     },
+    GetAvatar {
+        #[arg(long)]
+        contact: Option<String>,
+        #[arg(long)]
+        profile: Option<String>,
+        #[arg(short = 'g', long = "group-id")]
+        group_id: Option<String>,
+    },
+    GetSticker {
+        #[arg(long = "pack-id")]
+        pack_id: String,
+        #[arg(long = "sticker-id")]
+        sticker_id: u32,
+    },
     GetUserStatus {
         recipient: Vec<String>,
     },
index cb1b4f8c1985c32e6fb2d025d4ce07b166a75022..211eaad820266645a794090b1e4ae36fbb1f39f6 100644 (file)
@@ -45,7 +45,24 @@ pub trait Rpc {
         account: Option<String>,
         id: String,
         recipient: Option<String>,
-        group_id: Option<String>,
+        #[allow(non_snake_case)] groupId: Option<String>,
+    ) -> Result<Value, ErrorObjectOwned>;
+
+    #[method(name = "getAvatar", param_kind = map)]
+    fn get_avatar(
+        &self,
+        account: Option<String>,
+        contact: Option<String>,
+        profile: Option<String>,
+        #[allow(non_snake_case)] groupId: Option<String>,
+    ) -> Result<Value, ErrorObjectOwned>;
+
+    #[method(name = "getSticker", param_kind = map)]
+    fn get_sticker(
+        &self,
+        account: Option<String>,
+        #[allow(non_snake_case)] packId: String,
+        #[allow(non_snake_case)] stickerId: u32,
     ) -> Result<Value, ErrorObjectOwned>;
 
     #[method(name = "getUserStatus", param_kind = map)]
index a48584408920e8a835cdd0f8fd4e34c2b9ac7e5e..61cbabf2cb66b415f6a2a9ef3e90f43bdcac80d9 100644 (file)
@@ -407,6 +407,19 @@ async fn handle_command(
                 .get_attachment(cli.account, id, recipient, group_id)
                 .await
         }
+        CliCommands::GetAvatar {
+            contact,
+            profile,
+            group_id,
+        } => {
+            client
+                .get_avatar(cli.account, contact, profile, group_id)
+                .await
+        }
+        CliCommands::GetSticker {
+            pack_id,
+            sticker_id,
+        } => client.get_sticker(cli.account, pack_id, sticker_id).await,
         CliCommands::StartChangeNumber {
             number,
             voice,
index 6fe1a67f8cbca13a7492cb4bbfa85a03ba5ddefe..e9eeb7c4c08acbe2f68ad4584e80458e69f52232 100644 (file)
@@ -34,6 +34,7 @@ import org.asamk.signal.manager.api.RecipientIdentifier;
 import org.asamk.signal.manager.api.SendGroupMessageResults;
 import org.asamk.signal.manager.api.SendMessageResults;
 import org.asamk.signal.manager.api.StickerPack;
+import org.asamk.signal.manager.api.StickerPackId;
 import org.asamk.signal.manager.api.StickerPackInvalidException;
 import org.asamk.signal.manager.api.StickerPackUrl;
 import org.asamk.signal.manager.api.TypingAction;
@@ -307,6 +308,14 @@ public interface Manager extends Closeable {
 
     InputStream retrieveAttachment(final String id) throws IOException;
 
+    InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
+
+    InputStream retrieveProfileAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
+
+    InputStream retrieveGroupAvatar(final GroupId groupId) throws IOException;
+
+    InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException;
+
     @Override
     void close();
 
index ccb18708404c978c444166afe10c44ce98a38bf3..e6b5a957eb2a6db05ef03584c95000f7025a2259 100644 (file)
@@ -1,7 +1,8 @@
 package org.asamk.signal.manager.api;
 
+import org.whispersystems.signalservice.internal.util.Hex;
+
 import java.util.Arrays;
-import java.util.Base64;
 
 public class StickerPackId {
 
@@ -36,6 +37,6 @@ public class StickerPackId {
 
     @Override
     public String toString() {
-        return "StickerPackId{" + Base64.getUrlEncoder().encodeToString(id) + '}';
+        return "StickerPackId{" + Hex.toStringCondensed(id) + '}';
     }
 }
index 9814d95c8b5f0ca31c464e338e9404276df207bd..b059e821ae066313a7483d162f47222808655570 100644 (file)
@@ -104,6 +104,7 @@ import org.whispersystems.signalservice.internal.util.Util;
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
@@ -1337,6 +1338,58 @@ public class ManagerImpl implements Manager {
         return context.getAttachmentHelper().retrieveAttachment(id).getStream();
     }
 
+    @Override
+    public InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
+        final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
+        final var address = account.getRecipientStore().resolveRecipientAddress(recipientId);
+        final var streamDetails = context.getAvatarStore().retrieveContactAvatar(address);
+        if (streamDetails == null) {
+            throw new FileNotFoundException();
+        }
+        return streamDetails.getStream();
+    }
+
+    @Override
+    public InputStream retrieveProfileAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
+        final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
+        context.getProfileHelper().getRecipientProfile(recipientId);
+        final var address = account.getRecipientStore().resolveRecipientAddress(recipientId);
+        final var streamDetails = context.getAvatarStore().retrieveProfileAvatar(address);
+        if (streamDetails == null) {
+            throw new FileNotFoundException();
+        }
+        return streamDetails.getStream();
+    }
+
+    @Override
+    public InputStream retrieveGroupAvatar(final GroupId groupId) throws IOException {
+        final var streamDetails = context.getAvatarStore().retrieveGroupAvatar(groupId);
+        context.getGroupHelper().getGroup(groupId);
+        if (streamDetails == null) {
+            throw new FileNotFoundException();
+        }
+        return streamDetails.getStream();
+    }
+
+    @Override
+    public InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException {
+        var streamDetails = context.getStickerPackStore().retrieveSticker(stickerPackId, stickerId);
+        if (streamDetails == null) {
+            final var pack = account.getStickerStore().getStickerPack(stickerPackId);
+            if (pack != null) {
+                try {
+                    context.getStickerHelper().retrieveStickerPack(stickerPackId, pack.packKey());
+                } catch (InvalidMessageException e) {
+                    logger.warn("Failed to download sticker pack");
+                }
+            }
+        }
+        if (streamDetails == null) {
+            throw new FileNotFoundException();
+        }
+        return streamDetails.getStream();
+    }
+
     @Override
     public void close() {
         Thread thread;
index af11a2df5daf5508fe06a78c8f16c1bfbb163a90..c629d55022a2ef02a4deea7a6a4711aced29ddfc 100644 (file)
@@ -722,6 +722,31 @@ Referred to generally as recipient.
 *-g* [GROUP], *--group-id* [GROUP]::
 Alternatively, specify the group IDs for which to get the attachment.
 
+=== getAvatar
+
+Gets the raw data for a specified contact, contact's profile or group avatar.
+The attachment data is returned as a Base64 String.
+
+*--contact* [RECIPIENT]::
+Specify the number of a recipient.
+
+*--profile* [RECIPIENT]::
+Specify the number of a recipient.
+
+*-g* [GROUP], *--group-id* [GROUP]::
+Alternatively, specify the group ID for which to get the avatar.
+
+=== getSticker
+
+Gets the raw data for a specified sticker.
+The attachment data is returned as a Base64 String.
+
+*--pack-id* [PACK_ID]::
+Specify the id of a sticker pack (hex encoded).
+
+*--sticker-id* [STICKER_ID]::
+Specify the index of a sticker in the sticker pack.
+
 === daemon
 
 signal-cli can run in daemon mode and provides JSON-RPC or an experimental dbus interface.
index 0f26c9945828a586a69c10a270d51bac909a90dd..a963ce4eab3575188b790b04bb88b8d286810cee 100644 (file)
@@ -17,6 +17,8 @@ public class Commands {
         addCommand(new FinishChangeNumberCommand());
         addCommand(new FinishLinkCommand());
         addCommand(new GetAttachmentCommand());
+        addCommand(new GetAvatarCommand());
+        addCommand(new GetStickerCommand());
         addCommand(new GetUserStatusCommand());
         addCommand(new AddStickerPackCommand());
         addCommand(new JoinGroupCommand());
index 160e53bcfd6423528dcc71d63c900fa2c5d79384..99ba1ecaf332e79320eb45eac76ba87c1af256d1 100644 (file)
@@ -26,6 +26,7 @@ public class GetAttachmentCommand implements JsonRpcLocalCommand {
 
     @Override
     public void attachToSubparser(final Subparser subparser) {
+        subparser.help("Retrieve an already downloaded attachment base64 encoded.");
         subparser.addArgument("--id").required(true).help("The ID of the attachment file.");
         var mut = subparser.addMutuallyExclusiveGroup().required(true);
         mut.addArgument("--recipient").help("Sender of the attachment");
diff --git a/src/main/java/org/asamk/signal/commands/GetAvatarCommand.java b/src/main/java/org/asamk/signal/commands/GetAvatarCommand.java
new file mode 100644 (file)
index 0000000..c757346
--- /dev/null
@@ -0,0 +1,76 @@
+package org.asamk.signal.commands;
+
+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.json.JsonAttachmentData;
+import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.api.UnregisteredRecipientException;
+import org.asamk.signal.output.JsonWriter;
+import org.asamk.signal.output.OutputWriter;
+import org.asamk.signal.output.PlainTextWriter;
+import org.asamk.signal.util.CommandUtil;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Base64;
+
+public class GetAvatarCommand implements JsonRpcLocalCommand {
+
+    @Override
+    public String getName() {
+        return "getAvatar";
+    }
+
+    @Override
+    public void attachToSubparser(final Subparser subparser) {
+        subparser.help("Retrieve the avatar of a contact, contact's profile or group base64 encoded.");
+        var mut = subparser.addMutuallyExclusiveGroup().required(true);
+        mut.addArgument("-c", "--contact").help("Get a contact avatar");
+        mut.addArgument("-p", "--profile").help("Get a profile avatar");
+        mut.addArgument("-g", "--group-id").help("Get a group avatar");
+    }
+
+    @Override
+    public void handleCommand(
+            final Namespace ns, final Manager m, final OutputWriter outputWriter
+    ) throws CommandException {
+        final var contactRecipient = ns.getString("contact");
+        final var profileRecipient = ns.getString("profile");
+        final var groupId = ns.getString("groupId");
+
+        final InputStream data;
+        try {
+            if (contactRecipient != null) {
+                data = m.retrieveContactAvatar(CommandUtil.getSingleRecipientIdentifier(contactRecipient,
+                        m.getSelfNumber()));
+            } else if (profileRecipient != null) {
+                data = m.retrieveProfileAvatar(CommandUtil.getSingleRecipientIdentifier(profileRecipient,
+                        m.getSelfNumber()));
+            } else {
+                data = m.retrieveGroupAvatar(CommandUtil.getGroupId(groupId));
+            }
+        } catch (FileNotFoundException ex) {
+            throw new UserErrorException("Could not find avatar", ex);
+        } catch (IOException ex) {
+            throw new UnexpectedErrorException("An error occurred reading avatar", ex);
+        } catch (UnregisteredRecipientException e) {
+            throw new UserErrorException("The user " + e.getSender().getIdentifier() + " is not registered.");
+        }
+
+        try (data) {
+            final var bytes = data.readAllBytes();
+            final var base64 = Base64.getEncoder().encodeToString(bytes);
+            switch (outputWriter) {
+                case PlainTextWriter writer -> writer.println(base64);
+                case JsonWriter writer -> writer.write(new JsonAttachmentData(base64));
+            }
+        } catch (IOException ex) {
+            throw new UnexpectedErrorException("An error occurred reading avatar", ex);
+        }
+    }
+}
diff --git a/src/main/java/org/asamk/signal/commands/GetStickerCommand.java b/src/main/java/org/asamk/signal/commands/GetStickerCommand.java
new file mode 100644 (file)
index 0000000..aba5b26
--- /dev/null
@@ -0,0 +1,60 @@
+package org.asamk.signal.commands;
+
+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.json.JsonAttachmentData;
+import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.api.StickerPackId;
+import org.asamk.signal.output.JsonWriter;
+import org.asamk.signal.output.OutputWriter;
+import org.asamk.signal.output.PlainTextWriter;
+import org.asamk.signal.util.Hex;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Base64;
+
+public class GetStickerCommand implements JsonRpcLocalCommand {
+
+    @Override
+    public String getName() {
+        return "getSticker";
+    }
+
+    @Override
+    public void attachToSubparser(final Subparser subparser) {
+        subparser.help("Retrieve the sticker of a sticker pack base64 encoded.");
+        subparser.addArgument("--pack-id").required(true).help("The ID of the sticker pack.");
+        subparser.addArgument("--sticker-id").type(int.class).required(true).help("The ID of the sticker.");
+    }
+
+    @Override
+    public void handleCommand(
+            final Namespace ns, final Manager m, final OutputWriter outputWriter
+    ) throws CommandException {
+
+        final var packId = StickerPackId.deserialize(Hex.toByteArray(ns.getString("pack-id")));
+        final var stickerId = ns.getInt("sticker-id");
+
+        try (InputStream data = m.retrieveSticker(packId, stickerId)) {
+            final var bytes = data.readAllBytes();
+            final var base64 = Base64.getEncoder().encodeToString(bytes);
+            switch (outputWriter) {
+                case PlainTextWriter writer -> writer.println(base64);
+                case JsonWriter writer -> writer.write(new JsonAttachmentData(base64));
+            }
+        } catch (FileNotFoundException ex) {
+            throw new UserErrorException("Could not find sticker with ID: " + stickerId + " in pack " + packId, ex);
+        } catch (IOException ex) {
+            throw new UnexpectedErrorException("An error occurred reading sticker with ID: "
+                    + stickerId
+                    + " in pack "
+                    + packId, ex);
+        }
+    }
+}
index f56a3ba48a8de57b61348bc983896e5e4ac30641..a37fa3e3b526fbf40ad9edc948192c9b08505d2b 100644 (file)
@@ -38,6 +38,7 @@ import org.asamk.signal.manager.api.RecipientIdentifier;
 import org.asamk.signal.manager.api.SendGroupMessageResults;
 import org.asamk.signal.manager.api.SendMessageResults;
 import org.asamk.signal.manager.api.StickerPack;
+import org.asamk.signal.manager.api.StickerPackId;
 import org.asamk.signal.manager.api.StickerPackInvalidException;
 import org.asamk.signal.manager.api.StickerPackUrl;
 import org.asamk.signal.manager.api.TypingAction;
@@ -1069,6 +1070,26 @@ public class DbusManagerImpl implements Manager {
         throw new UnsupportedOperationException();
     }
 
+    @Override
+    public InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public InputStream retrieveProfileAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public InputStream retrieveGroupAvatar(final GroupId groupId) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
     @SuppressWarnings("unchecked")
     private <T> T getValue(
             final Map<String, Variant<?>> stringVariantMap, final String field