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
#[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>,
},
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)]
.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,
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;
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();
package org.asamk.signal.manager.api;
+import org.whispersystems.signalservice.internal.util.Hex;
+
import java.util.Arrays;
-import java.util.Base64;
public class StickerPackId {
@Override
public String toString() {
- return "StickerPackId{" + Base64.getUrlEncoder().encodeToString(id) + '}';
+ return "StickerPackId{" + Hex.toStringCondensed(id) + '}';
}
}
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;
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;
*-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.
addCommand(new FinishChangeNumberCommand());
addCommand(new FinishLinkCommand());
addCommand(new GetAttachmentCommand());
+ addCommand(new GetAvatarCommand());
+ addCommand(new GetStickerCommand());
addCommand(new GetUserStatusCommand());
addCommand(new AddStickerPackCommand());
addCommand(new JoinGroupCommand());
@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");
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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);
+ }
+ }
+}
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;
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