mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 02:20:39 +00:00
Added base64 encoded attachment support (#966)
* Added base64 encoded attachment support * Added final * Added full RFC 2397 support * Added feedback * Update doc * Update signal-cli.1.adoc Co-authored-by: Sebastian Scheibner <asamk@gmx.de>
This commit is contained in:
parent
63e94a9fb4
commit
cb5e3c6bf7
6 changed files with 110 additions and 11 deletions
|
@ -51,7 +51,7 @@ public class AttachmentHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
|
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
|
||||||
var attachmentStream = AttachmentUtils.createAttachmentStream(new File(attachment));
|
var attachmentStream = AttachmentUtils.createAttachmentStream(attachment);
|
||||||
return uploadAttachment(attachmentStream);
|
return uploadAttachment(attachmentStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||||
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -19,17 +20,18 @@ public class AttachmentUtils {
|
||||||
}
|
}
|
||||||
final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
|
final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
|
||||||
for (var attachment : attachments) {
|
for (var attachment : attachments) {
|
||||||
signalServiceAttachments.add(createAttachmentStream(new File(attachment)));
|
signalServiceAttachments.add(createAttachmentStream(attachment));
|
||||||
}
|
}
|
||||||
return signalServiceAttachments;
|
return signalServiceAttachments;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalServiceAttachmentStream createAttachmentStream(File attachmentFile) throws AttachmentInvalidException {
|
public static SignalServiceAttachmentStream createAttachmentStream(String attachment) throws AttachmentInvalidException {
|
||||||
try {
|
try {
|
||||||
final var streamDetails = Utils.createStreamDetailsFromFile(attachmentFile);
|
final var streamDetails = Utils.createStreamDetails(attachment);
|
||||||
return createAttachmentStream(streamDetails, Optional.of(attachmentFile.getName()));
|
|
||||||
|
return createAttachmentStream(streamDetails.first(), streamDetails.second());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new AttachmentInvalidException(attachmentFile.toString(), e);
|
throw new AttachmentInvalidException(attachment, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
70
lib/src/main/java/org/asamk/signal/manager/util/DataURI.java
Normal file
70
lib/src/main/java/org/asamk/signal/manager/util/DataURI.java
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package org.asamk.signal.manager.util;
|
package org.asamk.signal.manager.util;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.api.Pair;
|
||||||
import org.signal.libsignal.protocol.IdentityKey;
|
import org.signal.libsignal.protocol.IdentityKey;
|
||||||
import org.signal.libsignal.protocol.fingerprint.Fingerprint;
|
import org.signal.libsignal.protocol.fingerprint.Fingerprint;
|
||||||
import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator;
|
import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator;
|
||||||
|
@ -9,6 +10,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -17,9 +19,11 @@ import java.net.URLConnection;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Spliterator;
|
import java.util.Spliterator;
|
||||||
import java.util.Spliterators;
|
import java.util.Spliterators;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
@ -31,10 +35,10 @@ public class Utils {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger(Utils.class);
|
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());
|
var mime = Files.probeContentType(file.toPath());
|
||||||
if (mime == null) {
|
if (mime == null) {
|
||||||
try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) {
|
try (final InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) {
|
||||||
mime = URLConnection.guessContentTypeFromStream(bufferedStream);
|
mime = URLConnection.guessContentTypeFromStream(bufferedStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,13 +48,31 @@ public class Utils {
|
||||||
return mime;
|
return mime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StreamDetails createStreamDetailsFromFile(File file) throws IOException {
|
public static Pair<StreamDetails, Optional<String>> createStreamDetailsFromDataURI(final String dataURI) {
|
||||||
InputStream stream = new FileInputStream(file);
|
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 size = file.length();
|
||||||
final var mime = getFileMimeType(file, "application/octet-stream");
|
final var mime = getFileMimeType(file, "application/octet-stream");
|
||||||
return new StreamDetails(stream, mime, size);
|
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(
|
public static Fingerprint computeSafetyNumber(
|
||||||
boolean isUuidCapable,
|
boolean isUuidCapable,
|
||||||
SignalServiceAddress ownAddress,
|
SignalServiceAddress ownAddress,
|
||||||
|
|
|
@ -231,6 +231,9 @@ Read the message from standard input.
|
||||||
|
|
||||||
*-a* [ATTACHMENT [ATTACHMENT ...]], *--attachment* [ATTACHMENT [ATTACHMENT ...]]::
|
*-a* [ATTACHMENT [ATTACHMENT ...]], *--attachment* [ATTACHMENT [ATTACHMENT ...]]::
|
||||||
Add one or more files as 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::
|
*--sticker* STICKER::
|
||||||
Send a sticker of a locally known sticker pack (syntax: stickerPackId:stickerId).
|
Send a sticker of a locally known sticker pack (syntax: stickerPackId:stickerId).
|
||||||
|
|
|
@ -55,7 +55,9 @@ public class SendCommand implements JsonRpcLocalCommand {
|
||||||
mut.addArgument("--message-from-stdin")
|
mut.addArgument("--message-from-stdin")
|
||||||
.action(Arguments.storeTrue())
|
.action(Arguments.storeTrue())
|
||||||
.help("Read the message from standard input.");
|
.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")
|
subparser.addArgument("-e", "--end-session", "--endsession")
|
||||||
.help("Clear session state and send end session message.")
|
.help("Clear session state and send end session message.")
|
||||||
.action(Arguments.storeTrue());
|
.action(Arguments.storeTrue());
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue