From: AsamK Date: Fri, 9 Feb 2024 21:06:46 +0000 (+0100) Subject: Add command to retrieve avatars and stickers X-Git-Tag: v0.13.0~21 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/7cf3a989bf47dd81f8700dcfb6986ac81e1bf02e Add command to retrieve avatars and stickers Fixes #1125 --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 39950184..fa0aff9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/client/src/cli.rs b/client/src/cli.rs index dea3a7df..33403890 100644 --- a/client/src/cli.rs +++ b/client/src/cli.rs @@ -68,6 +68,20 @@ pub enum CliCommands { #[arg(short = 'g', long = "group-id")] group_id: Option, }, + GetAvatar { + #[arg(long)] + contact: Option, + #[arg(long)] + profile: Option, + #[arg(short = 'g', long = "group-id")] + group_id: Option, + }, + GetSticker { + #[arg(long = "pack-id")] + pack_id: String, + #[arg(long = "sticker-id")] + sticker_id: u32, + }, GetUserStatus { recipient: Vec, }, diff --git a/client/src/jsonrpc.rs b/client/src/jsonrpc.rs index cb1b4f8c..211eaad8 100644 --- a/client/src/jsonrpc.rs +++ b/client/src/jsonrpc.rs @@ -45,7 +45,24 @@ pub trait Rpc { account: Option, id: String, recipient: Option, - group_id: Option, + #[allow(non_snake_case)] groupId: Option, + ) -> Result; + + #[method(name = "getAvatar", param_kind = map)] + fn get_avatar( + &self, + account: Option, + contact: Option, + profile: Option, + #[allow(non_snake_case)] groupId: Option, + ) -> Result; + + #[method(name = "getSticker", param_kind = map)] + fn get_sticker( + &self, + account: Option, + #[allow(non_snake_case)] packId: String, + #[allow(non_snake_case)] stickerId: u32, ) -> Result; #[method(name = "getUserStatus", param_kind = map)] diff --git a/client/src/main.rs b/client/src/main.rs index a4858440..61cbabf2 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -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, 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 6fe1a67f..e9eeb7c4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -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(); diff --git a/lib/src/main/java/org/asamk/signal/manager/api/StickerPackId.java b/lib/src/main/java/org/asamk/signal/manager/api/StickerPackId.java index ccb18708..e6b5a957 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/StickerPackId.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/StickerPackId.java @@ -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) + '}'; } } 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 9814d95c..b059e821 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 @@ -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; diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index af11a2df..c629d550 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -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. diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 0f26c994..a963ce4e 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -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()); diff --git a/src/main/java/org/asamk/signal/commands/GetAttachmentCommand.java b/src/main/java/org/asamk/signal/commands/GetAttachmentCommand.java index 160e53bc..99ba1eca 100644 --- a/src/main/java/org/asamk/signal/commands/GetAttachmentCommand.java +++ b/src/main/java/org/asamk/signal/commands/GetAttachmentCommand.java @@ -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 index 00000000..c7573465 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/GetAvatarCommand.java @@ -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 index 00000000..aba5b26c --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/GetStickerCommand.java @@ -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); + } + } +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index f56a3ba4..a37fa3e3 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -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 getValue( final Map> stringVariantMap, final String field