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