]> nmode's Git Repositories - signal-cli/commitdiff
Add command to get an attachment (#1080)
authorced-b <cedric@cos.flag.org>
Tue, 1 Nov 2022 21:47:43 +0000 (17:47 -0400)
committerGitHub <noreply@github.com>
Tue, 1 Nov 2022 21:47:43 +0000 (22:47 +0100)
* 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 <cedb@keylimebox.org>
lib/src/main/java/org/asamk/signal/manager/AttachmentStore.java
lib/src/main/java/org/asamk/signal/manager/Manager.java
lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java
man/signal-cli.1.adoc
src/main/java/org/asamk/signal/commands/Commands.java
src/main/java/org/asamk/signal/commands/GetAttachmentCommand.java [new file with mode: 0644]
src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
src/main/java/org/asamk/signal/json/JsonAttachmentData.java [new file with mode: 0644]
src/main/java/org/asamk/signal/json/JsonStreamSerializer.java [new file with mode: 0644]

index 949e28629f9492202d5af8c36799883892a7ea4e..e3c96ad6eb66d36a3525642bfb861623b193616d 100644 (file)
@@ -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.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.messages.SignalServiceAttachmentPointer;
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
+import org.whispersystems.signalservice.api.util.StreamDetails;
 
 import java.io.File;
 import java.io.FileOutputStream;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -39,6 +41,14 @@ public class AttachmentStore {
                 Optional.ofNullable(pointer.getContentType()));
     }
 
                 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)) {
     private void storeAttachment(final File attachmentFile, final AttachmentStorer storer) throws IOException {
         createAttachmentsDir();
         try (OutputStream output = new FileOutputStream(attachmentFile)) {
index 01d88e46c43b669c13395ad77f30cdb01a58c0ca..75503790e0d7a8abae5fc3313953ab1dfc5d63d8 100644 (file)
@@ -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.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;
 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);
 
 
     void addClosedListener(Runnable listener);
 
+    InputStream retrieveAttachment(final String id) throws IOException;
+
     @Override
     void close() throws IOException;
 
     @Override
     void close() throws IOException;
 
index 3123163022a531a327790aa68052fc4a391f0a7d..aead1a28805d7f029e665265412a596a3abeb2c4 100644 (file)
@@ -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.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;
 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;
     @Override
     public void close() {
         Thread thread;
index ea5739b385907833d55beb2cea6f4a461a1da6f5..b4b0d04b9d3d2300bd5145da58babe853346bb99 100644 (file)
@@ -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.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;
 
 import java.io.File;
 import java.io.IOException;
@@ -38,6 +39,11 @@ public class AttachmentHelper {
         return attachmentStore.getAttachmentFile(pointer);
     }
 
         return attachmentStore.getAttachmentFile(pointer);
     }
 
+    public StreamDetails retrieveAttachment(final String id) throws IOException {
+        return attachmentStore.retrieveAttachment(id);
+    }
+
+
     public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
         var attachmentStreams = AttachmentUtils.createAttachmentStreams(attachments);
 
     public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
         var attachmentStreams = AttachmentUtils.createAttachmentStreams(attachments);
 
index 4d34582aa83341fde7d3c6c782bc27a5c5d968e2..be940418e001044f6eecabdee3048f3e803aaba5 100644 (file)
@@ -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.
 
 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.
 === daemon
 
 signal-cli can run in daemon mode and provides an experimental dbus or JSON-RPC interface.
index 1c4251da504617c9cd66c629ef53e271eaa8bcb2..1f0fc2ec48bb92eb609e0899b5f26fa8f777b81f 100644 (file)
@@ -11,6 +11,7 @@ public class Commands {
 
     static {
         addCommand(new AddDeviceCommand());
 
     static {
         addCommand(new AddDeviceCommand());
+        addCommand(new GetAttachmentCommand());
         addCommand(new BlockCommand());
         addCommand(new DaemonCommand());
         addCommand(new DeleteLocalAccountDataCommand());
         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 (file)
index 0000000..560be9f
--- /dev/null
@@ -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);
+        }
+    }
+}
index b59be923d0c0091ab8a3935166b3dc72690305f7..c0dc1cf832ac8e68f4b4b06ff050fc1dbbc276d2 100644 (file)
@@ -46,6 +46,7 @@ import org.freedesktop.dbus.types.Variant;
 
 import java.io.File;
 import java.io.IOException;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.time.Duration;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.time.Duration;
@@ -916,6 +917,11 @@ public class DbusManagerImpl implements Manager {
         }).toList();
     }
 
         }).toList();
     }
 
+    @Override
+    public InputStream retrieveAttachment(final String id) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
     @SuppressWarnings("unchecked")
     private <T> T getValue(
             final Map<String, Variant<?>> stringVariantMap, final String field
     @SuppressWarnings("unchecked")
     private <T> T getValue(
             final Map<String, Variant<?>> 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 (file)
index 0000000..004f02d
--- /dev/null
@@ -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 (file)
index 0000000..6eb359c
--- /dev/null
@@ -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<InputStream> {
+
+    @Override
+    public void serialize(
+            final InputStream value,
+            final JsonGenerator jsonGenerator,
+            final SerializerProvider serializers
+    ) throws IOException {
+        jsonGenerator.writeBinary(value, -1);
+    }
+}