]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java
b55153f061764a6cbbcc22eb6f696b580d41e0a9
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / AttachmentHelper.java
1 package org.asamk.signal.manager.helper;
2
3 import org.asamk.signal.manager.api.AttachmentInvalidException;
4 import org.asamk.signal.manager.config.ServiceConfig;
5 import org.asamk.signal.manager.internal.SignalDependencies;
6 import org.asamk.signal.manager.storage.AttachmentStore;
7 import org.asamk.signal.manager.util.AttachmentUtils;
8 import org.asamk.signal.manager.util.IOUtils;
9 import org.signal.libsignal.protocol.InvalidMessageException;
10 import org.slf4j.Logger;
11 import org.slf4j.LoggerFactory;
12 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
13 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
14 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
15 import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
16 import org.whispersystems.signalservice.api.util.StreamDetails;
17
18 import java.io.File;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.OutputStream;
22 import java.nio.file.Files;
23 import java.util.ArrayList;
24 import java.util.List;
25
26 public class AttachmentHelper {
27
28 private static final Logger logger = LoggerFactory.getLogger(AttachmentHelper.class);
29
30 private final SignalDependencies dependencies;
31 private final AttachmentStore attachmentStore;
32
33 public AttachmentHelper(final Context context) {
34 this.dependencies = context.getDependencies();
35 this.attachmentStore = context.getAttachmentStore();
36 }
37
38 public File getAttachmentFile(SignalServiceAttachmentPointer pointer) {
39 return attachmentStore.getAttachmentFile(pointer);
40 }
41
42 public StreamDetails retrieveAttachment(final String id) throws IOException {
43 return attachmentStore.retrieveAttachment(id);
44 }
45
46 public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
47 final var attachmentStreams = createAttachmentStreams(attachments);
48
49 try {
50 // Upload attachments here, so we only upload once even for multiple recipients
51 final var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
52 for (final var attachmentStream : attachmentStreams) {
53 attachmentPointers.add(uploadAttachment(attachmentStream));
54 }
55 return attachmentPointers;
56 } finally {
57 for (final var attachmentStream : attachmentStreams) {
58 attachmentStream.close();
59 }
60 }
61 }
62
63 private List<SignalServiceAttachmentStream> createAttachmentStreams(List<String> attachments) throws AttachmentInvalidException, IOException {
64 if (attachments == null) {
65 return null;
66 }
67 final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
68 for (var attachment : attachments) {
69 final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
70 signalServiceAttachments.add(AttachmentUtils.createAttachmentStream(attachment, uploadSpec));
71 }
72 return signalServiceAttachments;
73 }
74
75 public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
76 final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
77 var attachmentStream = AttachmentUtils.createAttachmentStream(attachment, uploadSpec);
78 return uploadAttachment(attachmentStream);
79 }
80
81 public SignalServiceAttachmentPointer uploadAttachment(SignalServiceAttachmentStream attachment) throws IOException {
82 var messageSender = dependencies.getMessageSender();
83 return messageSender.uploadAttachment(attachment);
84 }
85
86 public void downloadAttachment(final SignalServiceAttachment attachment) {
87 if (!attachment.isPointer()) {
88 logger.warn("Invalid state, can't store an attachment stream.");
89 }
90
91 var pointer = attachment.asPointer();
92 if (pointer.getPreview().isPresent()) {
93 final var preview = pointer.getPreview().get();
94 try {
95 attachmentStore.storeAttachmentPreview(pointer,
96 outputStream -> outputStream.write(preview, 0, preview.length));
97 } catch (IOException e) {
98 logger.warn("Failed to download attachment preview, ignoring: {}", e.getMessage());
99 }
100 }
101
102 try {
103 attachmentStore.storeAttachment(pointer, outputStream -> this.retrieveAttachment(pointer, outputStream));
104 } catch (IOException e) {
105 logger.warn("Failed to download attachment ({}), ignoring: {}", pointer.getRemoteId(), e.getMessage());
106 }
107 }
108
109 void retrieveAttachment(SignalServiceAttachment attachment, OutputStream outputStream) throws IOException {
110 retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream));
111 }
112
113 public void retrieveAttachment(SignalServiceAttachment attachment, AttachmentHandler consumer) throws IOException {
114 if (attachment.isStream()) {
115 var input = attachment.asStream().getInputStream();
116 // don't close input stream here, it might be reused later (e.g. with contact sync messages ...)
117 consumer.handle(input);
118 return;
119 }
120
121 final var pointer = attachment.asPointer();
122 logger.debug("Retrieving attachment {} with size {}", pointer.getRemoteId(), pointer.getSize());
123 var tmpFile = IOUtils.createTempFile();
124 try (var input = retrieveAttachmentAsStream(pointer, tmpFile)) {
125 consumer.handle(input);
126 } finally {
127 try {
128 Files.delete(tmpFile.toPath());
129 } catch (IOException e) {
130 logger.warn("Failed to delete received attachment temp file “{}”, ignoring: {}",
131 tmpFile,
132 e.getMessage());
133 }
134 }
135 }
136
137 private InputStream retrieveAttachmentAsStream(
138 SignalServiceAttachmentPointer pointer,
139 File tmpFile
140 ) throws IOException {
141 try {
142 return dependencies.getMessageReceiver()
143 .retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE);
144 } catch (MissingConfigurationException | InvalidMessageException e) {
145 throw new IOException(e);
146 }
147 }
148
149 @FunctionalInterface
150 public interface AttachmentHandler {
151
152 void handle(InputStream inputStream) throws IOException;
153 }
154 }