"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":[] }
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;
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
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;
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;
@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);
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) {
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 {
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;
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);
}
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);
}
}
--- /dev/null
+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);
+ }
+}
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) {}
}
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());
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);
return new StickerStore(stickers, saver);
}
- public Sticker getSticker(StickerPackId packId) {
+ public Sticker getStickerPack(StickerPackId packId) {
synchronized (stickers) {
return stickers.get(packId);
}
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.
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.
*-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.
*--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.
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;
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;
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
}
}
+ 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());
+ }
}
}
: 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()
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);
}
}
}
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)));
+ }
}
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;
@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()));
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());
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());
@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());