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 <cedb@keylimebox.org>
This commit is contained in:
ced-b 2022-11-01 17:47:43 -04:00 committed by GitHub
parent bf76c04664
commit 2e4d346bc8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 138 additions and 0 deletions

View file

@ -2,8 +2,10 @@ package org.asamk.signal.manager;
import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.MimeUtils; 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.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; 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 { private void storeAttachment(final File attachmentFile, final AttachmentStorer storer) throws IOException {
createAttachmentsDir(); createAttachmentsDir();
try (OutputStream output = new FileOutputStream(attachmentFile)) { try (OutputStream output = new FileOutputStream(attachmentFile)) {

View file

@ -39,6 +39,7 @@ import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.time.Duration; import java.time.Duration;
import java.util.Collection; 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 @Override
void close() throws IOException; void close() throws IOException;

View file

@ -84,6 +84,7 @@ import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Duration; 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 @Override
public void close() { public void close() {
Thread thread; Thread thread;

View 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.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.File; import java.io.File;
import java.io.IOException; 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 { public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
var attachmentStreams = AttachmentUtils.createAttachmentStreams(attachments); var attachmentStreams = AttachmentUtils.createAttachmentStreams(attachments);

View file

@ -635,6 +635,20 @@ The required manifest.json has the following format:
PATH:: PATH::
The path of the manifest.json or a zip file containing the sticker pack you wish to upload. 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 === daemon
signal-cli can run in daemon mode and provides an experimental dbus or JSON-RPC interface. signal-cli can run in daemon mode and provides an experimental dbus or JSON-RPC interface.

View file

@ -11,6 +11,7 @@ public class Commands {
static { static {
addCommand(new AddDeviceCommand()); addCommand(new AddDeviceCommand());
addCommand(new GetAttachmentCommand());
addCommand(new BlockCommand()); addCommand(new BlockCommand());
addCommand(new DaemonCommand()); addCommand(new DaemonCommand());
addCommand(new DeleteLocalAccountDataCommand()); addCommand(new DeleteLocalAccountDataCommand());

View file

@ -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);
}
}
}

View file

@ -46,6 +46,7 @@ import org.freedesktop.dbus.types.Variant;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.time.Duration; 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") @SuppressWarnings("unchecked")
private <T> T getValue( private <T> T getValue(
final Map<String, Variant<?>> stringVariantMap, final String field final Map<String, Variant<?>> stringVariantMap, final String field

View file

@ -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
) {}

View file

@ -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);
}
}