From: ced-b Date: Tue, 1 Nov 2022 21:47:43 +0000 (-0400) Subject: Add command to get an attachment (#1080) X-Git-Tag: v0.11.5~16 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/2e4d346bc826da5c35a17841772cf81ba4f98101?ds=sidebyside Add command to get an attachment (#1080) * Add command to get an attachment * Refactor retrieving of attachments to use StreamDetails * Refactor AttachmentCommand to GetAttachmentCommand * Minor improvements to GetAttachmentCommand * Use JSON serializer to serialize binary data Serializing the stream is better for memory handling than loading the whole thing into the file. * Clean up unneeded class * Added command to doc Co-authored-by: cedb --- diff --git a/lib/src/main/java/org/asamk/signal/manager/AttachmentStore.java b/lib/src/main/java/org/asamk/signal/manager/AttachmentStore.java index 949e2862..e3c96ad6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/AttachmentStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/AttachmentStore.java @@ -2,8 +2,10 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.manager.util.MimeUtils; +import org.asamk.signal.manager.util.Utils; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; +import org.whispersystems.signalservice.api.util.StreamDetails; import java.io.File; import java.io.FileOutputStream; @@ -39,6 +41,14 @@ public class AttachmentStore { Optional.ofNullable(pointer.getContentType())); } + public StreamDetails retrieveAttachment(final String id) throws IOException { + final var attachmentFile = new File(attachmentsPath, id); + if (!attachmentFile.exists()) { + return null; + } + return Utils.createStreamDetailsFromFile(attachmentFile); + } + private void storeAttachment(final File attachmentFile, final AttachmentStorer storer) throws IOException { createAttachmentsDir(); try (OutputStream output = new FileOutputStream(attachmentFile)) { 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 01d88e46..75503790 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -39,6 +39,7 @@ import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.Closeable; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.time.Duration; import java.util.Collection; @@ -271,6 +272,8 @@ public interface Manager extends Closeable { void addClosedListener(Runnable listener); + InputStream retrieveAttachment(final String id) throws IOException; + @Override void close() throws IOException; diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 31231630..aead1a28 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -84,6 +84,7 @@ import org.whispersystems.signalservice.internal.util.Util; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -1167,6 +1168,11 @@ class ManagerImpl implements Manager { } } + @Override + public InputStream retrieveAttachment(final String id) throws IOException { + return context.getAttachmentHelper().retrieveAttachment(id).getStream(); + } + @Override public void close() { Thread thread; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java index ea5739b3..b4b0d04b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -13,6 +13,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; +import org.whispersystems.signalservice.api.util.StreamDetails; import java.io.File; import java.io.IOException; @@ -38,6 +39,11 @@ public class AttachmentHelper { return attachmentStore.getAttachmentFile(pointer); } + public StreamDetails retrieveAttachment(final String id) throws IOException { + return attachmentStore.retrieveAttachment(id); + } + + public List uploadAttachments(final List attachments) throws AttachmentInvalidException, IOException { var attachmentStreams = AttachmentUtils.createAttachmentStreams(attachments); diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 4d34582a..be940418 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -635,6 +635,20 @@ The required manifest.json has the following format: PATH:: The path of the manifest.json or a zip file containing the sticker pack you wish to upload. +=== getAttachment + +Gets teh raw data for a specified attachment. This is done using the ID of the attachment the recipient or group ID. +The attachment data is returned as a Base64 String. + +*--id* [ID]:: +The ID of the attachment as given in the attachment list of the message. + +*--recipient* [RECIPIENT]:: +Specify the number which sent the attachment. Referred to generally as recipient. + +*-g* [GROUP], *--group-id* [GROUP]:: +Alternatively, specify the group IDs that for which to get the attachment. + === daemon signal-cli can run in daemon mode and provides an experimental dbus or JSON-RPC interface. diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 1c4251da..1f0fc2ec 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -11,6 +11,7 @@ public class Commands { static { addCommand(new AddDeviceCommand()); + addCommand(new GetAttachmentCommand()); addCommand(new BlockCommand()); addCommand(new DaemonCommand()); addCommand(new DeleteLocalAccountDataCommand()); diff --git a/src/main/java/org/asamk/signal/commands/GetAttachmentCommand.java b/src/main/java/org/asamk/signal/commands/GetAttachmentCommand.java new file mode 100644 index 00000000..560be9f9 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/GetAttachmentCommand.java @@ -0,0 +1,63 @@ +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.output.JsonWriter; +import org.asamk.signal.output.OutputWriter; +import org.asamk.signal.output.PlainTextWriter; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Base64; + +public class GetAttachmentCommand implements JsonRpcLocalCommand { + + @Override + public String getName() { + return "getAttachment"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { + 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"); + mut.addArgument("-g", "--group-id") + .help("Group in which the attachment was received"); + } + + @Override + public void handleCommand( + final Namespace ns, + final Manager m, + final OutputWriter outputWriter + ) throws CommandException { + + final var id = ns.getString("id"); + + try(InputStream attachment = m.retrieveAttachment(id)) { + if (outputWriter instanceof PlainTextWriter writer) { + final var bytes = attachment.readAllBytes(); + final var base64 = Base64.getEncoder().encodeToString(bytes); + writer.println(base64); + } else if (outputWriter instanceof JsonWriter writer) { + writer.write(new JsonAttachmentData(attachment)); + } + } catch (FileNotFoundException ex) { + throw new UserErrorException("Could not find attachment with ID: " + id, ex); + } catch (IOException ex) { + throw new UnexpectedErrorException("An error occurred reading attachment: " + id, ex); + } + } +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index b59be923..c0dc1cf8 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -46,6 +46,7 @@ import org.freedesktop.dbus.types.Variant; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.time.Duration; @@ -916,6 +917,11 @@ public class DbusManagerImpl implements Manager { }).toList(); } + @Override + public InputStream retrieveAttachment(final String id) throws IOException { + throw new UnsupportedOperationException(); + } + @SuppressWarnings("unchecked") private T getValue( final Map> stringVariantMap, final String field diff --git a/src/main/java/org/asamk/signal/json/JsonAttachmentData.java b/src/main/java/org/asamk/signal/json/JsonAttachmentData.java new file mode 100644 index 00000000..004f02d8 --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonAttachmentData.java @@ -0,0 +1,9 @@ +package org.asamk.signal.json; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.io.InputStream; + +public record JsonAttachmentData( + @JsonSerialize(using=JsonStreamSerializer.class) InputStream data +) {} diff --git a/src/main/java/org/asamk/signal/json/JsonStreamSerializer.java b/src/main/java/org/asamk/signal/json/JsonStreamSerializer.java new file mode 100644 index 00000000..6eb359cd --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonStreamSerializer.java @@ -0,0 +1,20 @@ +package org.asamk.signal.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.io.InputStream; + +public class JsonStreamSerializer extends JsonSerializer { + + @Override + public void serialize( + final InputStream value, + final JsonGenerator jsonGenerator, + final SerializerProvider serializers + ) throws IOException { + jsonGenerator.writeBinary(value, -1); + } +}