}
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
- var attachmentStream = AttachmentUtils.createAttachmentStream(new File(attachment));
+ var attachmentStream = AttachmentUtils.createAttachmentStream(attachment);
return uploadAttachment(attachmentStream);
}
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
import java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
}
final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
for (var attachment : attachments) {
- signalServiceAttachments.add(createAttachmentStream(new File(attachment)));
+ signalServiceAttachments.add(createAttachmentStream(attachment));
}
return signalServiceAttachments;
}
- public static SignalServiceAttachmentStream createAttachmentStream(File attachmentFile) throws AttachmentInvalidException {
+ public static SignalServiceAttachmentStream createAttachmentStream(String attachment) throws AttachmentInvalidException {
try {
- final var streamDetails = Utils.createStreamDetailsFromFile(attachmentFile);
- return createAttachmentStream(streamDetails, Optional.of(attachmentFile.getName()));
+ final var streamDetails = Utils.createStreamDetails(attachment);
+
+ return createAttachmentStream(streamDetails.first(), streamDetails.second());
} catch (IOException e) {
- throw new AttachmentInvalidException(attachmentFile.toString(), e);
+ throw new AttachmentInvalidException(attachment, e);
}
}
--- /dev/null
+package org.asamk.signal.manager.util;
+
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@SuppressWarnings({"java:S6218"})
+public record DataURI(String mediaType, Map<String, String> parameter, byte[] data) {
+
+ public static final Pattern DATA_URI_PATTERN = Pattern.compile(
+ "\\Adata:(?<type>.+?/.+?)?(?<parameters>;.+?=.+?)?(?<base64>;base64)?,(?<data>.+)\\z",
+ Pattern.CASE_INSENSITIVE);
+ public static final Pattern PARAMETER_PATTERN = Pattern.compile("\\G;(?<key>.+)=(?<value>.+)",
+ Pattern.CASE_INSENSITIVE);
+ public static final String DEFAULT_TYPE = "text/plain";
+
+ /**
+ * Generates a new {@link DataURI} object that follows
+ * <a href="https://datatracker.ietf.org/doc/html/rfc2397">RFC 2397</a> from the given string.
+ * <p>
+ * The {@code dataURI} must be of the form:
+ * <p>
+ * {@code
+ * data:[<mediatype>][;base64],<data>
+ * }
+ * <p>
+ * The {@code <mediatype>} is an Internet media type specification (with
+ * optional parameters.) The appearance of ";base64" means that the data
+ * is encoded as base64. Without ";base64", the data is represented using (ASCII) URL Escaped encoding.
+ * If {@code <mediatype>} is omitted, it defaults to {@link DataURI#DEFAULT_TYPE}.
+ * Parameter values should use the URL Escaped encoding.
+ *
+ * @param dataURI the data URI
+ * @return a data URI object
+ * @throws IllegalArgumentException if the given string is not a valid data URI
+ */
+ public static DataURI of(final String dataURI) {
+ final var matcher = DATA_URI_PATTERN.matcher(dataURI);
+
+ if (!matcher.find()) {
+ throw new IllegalArgumentException("The given string is not a valid data URI.");
+ }
+
+ final Map<String, String> parameters = new HashMap<>();
+ final var params = matcher.group("parameters");
+ if (params != null) {
+ final Matcher paramsMatcher = PARAMETER_PATTERN.matcher(params);
+ while (paramsMatcher.find()) {
+ final var key = paramsMatcher.group("key");
+ final var value = URLDecoder.decode(paramsMatcher.group("value"), StandardCharsets.UTF_8);
+ parameters.put(key, value);
+ }
+ }
+
+ final boolean isBase64 = matcher.group("base64") != null;
+ final byte[] data;
+ if (isBase64) {
+ data = Base64.getDecoder().decode(matcher.group("data").getBytes(StandardCharsets.UTF_8));
+ } else {
+ data = URLDecoder.decode(matcher.group("data"), StandardCharsets.UTF_8).getBytes(StandardCharsets.UTF_8);
+ }
+
+ return new DataURI(Optional.ofNullable(matcher.group("type")).orElse(DEFAULT_TYPE), parameters, data);
+ }
+}
package org.asamk.signal.manager.util;
+import org.asamk.signal.manager.api.Pair;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.fingerprint.Fingerprint;
import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.util.Base64;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
+import java.util.Optional;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.BiFunction;
private final static Logger logger = LoggerFactory.getLogger(Utils.class);
- public static String getFileMimeType(File file, String defaultMimeType) throws IOException {
+ public static String getFileMimeType(final File file, final String defaultMimeType) throws IOException {
var mime = Files.probeContentType(file.toPath());
if (mime == null) {
- try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) {
+ try (final InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) {
mime = URLConnection.guessContentTypeFromStream(bufferedStream);
}
}
return mime;
}
- public static StreamDetails createStreamDetailsFromFile(File file) throws IOException {
- InputStream stream = new FileInputStream(file);
+ public static Pair<StreamDetails, Optional<String>> createStreamDetailsFromDataURI(final String dataURI) {
+ final DataURI uri = DataURI.of(dataURI);
+
+ return new Pair<>(new StreamDetails(
+ new ByteArrayInputStream(uri.data()), uri.mediaType(), uri.data().length),
+ Optional.ofNullable(uri.parameter().get("filename")));
+ }
+
+ public static StreamDetails createStreamDetailsFromFile(final File file) throws IOException {
+ final InputStream stream = new FileInputStream(file);
final var size = file.length();
final var mime = getFileMimeType(file, "application/octet-stream");
return new StreamDetails(stream, mime, size);
}
+ public static Pair<StreamDetails, Optional<String>> createStreamDetails(final String value) throws IOException {
+ try {
+ return createStreamDetailsFromDataURI(value);
+ } catch (final IllegalArgumentException e) {
+ final File f = new File(value);
+
+ return new Pair<>(createStreamDetailsFromFile(f), Optional.of(f.getName()));
+ }
+ }
+
public static Fingerprint computeSafetyNumber(
boolean isUuidCapable,
SignalServiceAddress ownAddress,
*-a* [ATTACHMENT [ATTACHMENT ...]], *--attachment* [ATTACHMENT [ATTACHMENT ...]]::
Add one or more files as attachment.
+Can be either a file path or a data URI. Data URI encoded attachments must follow the RFC 2397.
+Additionally a file name can be added:
+e.g.: `data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>`
*--sticker* STICKER::
Send a sticker of a locally known sticker pack (syntax: stickerPackId:stickerId).
mut.addArgument("--message-from-stdin")
.action(Arguments.storeTrue())
.help("Read the message from standard input.");
- subparser.addArgument("-a", "--attachment").nargs("*").help("Add file as attachment");
+ subparser.addArgument("-a", "--attachment").nargs("*").help("Add an attachment. "
+ + "Can be either a file path or a data URI. Data URI encoded attachments must follow the RFC 2397. Additionally a file name can be added, e.g. "
+ + "data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>.");
subparser.addArgument("-e", "--end-session", "--endsession")
.help("Clear session state and send end session message.")
.action(Arguments.storeTrue());