mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-09-19 05:58:57 +00:00
Add --attachment-dimensions and --attachment-blurhash to send (#2120)
* Accept caller-supplied attachment dimensions Forward positional WIDTHxHEIGHT metadata supplied by the caller. Validate dimensions before sending and retain automatic image detection for omitted entries. * Accept caller-supplied attachment BlurHashes Forward positional BlurHashes supplied by the caller alongside attachment dimensions. Reject BlurHashes whose length doesn't match their size digit. * Skip image probing when dimensions are supplied Caller-supplied dimensions replace the probed values, so don't buffer and decode the image to measure it.
This commit is contained in:
parent
a255a7fecf
commit
c146224493
@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- `send --attachment-dimensions` and `--attachment-blurhash` to set the placeholder shown before an attachment is downloaded
|
||||
|
||||
## [0.14.8] - 2026-09-10
|
||||
|
||||
### Added
|
||||
|
||||
@ -6,6 +6,8 @@ import java.util.Optional;
|
||||
public record Message(
|
||||
String messageText,
|
||||
List<String> attachments,
|
||||
List<AttachmentDimensions> attachmentDimensions,
|
||||
List<String> attachmentBlurHashes,
|
||||
boolean viewOnce,
|
||||
boolean voiceNote,
|
||||
List<Mention> mentions,
|
||||
@ -17,6 +19,8 @@ public record Message(
|
||||
boolean urgent
|
||||
) {
|
||||
|
||||
public record AttachmentDimensions(int width, int height) {}
|
||||
|
||||
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
|
||||
|
||||
public record Quote(
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||
import org.asamk.signal.manager.api.Message.AttachmentDimensions;
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||
import org.asamk.signal.manager.storage.AttachmentStore;
|
||||
@ -52,9 +53,11 @@ public class AttachmentHelper {
|
||||
|
||||
public List<SignalServiceAttachment> uploadAttachments(
|
||||
final List<String> attachments,
|
||||
final List<AttachmentDimensions> dimensions,
|
||||
final List<String> blurHashes,
|
||||
boolean voiceNote
|
||||
) throws AttachmentInvalidException, IOException {
|
||||
final var attachmentStreams = createAttachmentStreams(attachments, voiceNote);
|
||||
final var attachmentStreams = createAttachmentStreams(attachments, dimensions, blurHashes, voiceNote);
|
||||
|
||||
try {
|
||||
// Upload attachments here, so we only upload once even for multiple recipients
|
||||
@ -71,26 +74,31 @@ public class AttachmentHelper {
|
||||
}
|
||||
|
||||
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
|
||||
return uploadAttachments(attachments, false);
|
||||
return uploadAttachments(attachments, List.of(), List.of(), false);
|
||||
}
|
||||
|
||||
private List<SignalServiceAttachmentStream> createAttachmentStreams(
|
||||
List<String> attachments,
|
||||
List<AttachmentDimensions> dimensions,
|
||||
List<String> blurHashes,
|
||||
boolean voiceNote
|
||||
) throws AttachmentInvalidException, IOException {
|
||||
if (attachments == null) {
|
||||
return null;
|
||||
}
|
||||
final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
|
||||
for (var attachment : attachments) {
|
||||
final var attachmentStream = getAttachmentStream(attachment, voiceNote);
|
||||
signalServiceAttachments.add(attachmentStream);
|
||||
for (var i = 0; i < attachments.size(); i++) {
|
||||
final var size = i < dimensions.size() ? dimensions.get(i) : null;
|
||||
final var blurHash = i < blurHashes.size() && !blurHashes.get(i).isEmpty() ? blurHashes.get(i) : null;
|
||||
signalServiceAttachments.add(getAttachmentStream(attachments.get(i), size, blurHash, voiceNote));
|
||||
}
|
||||
return signalServiceAttachments;
|
||||
}
|
||||
|
||||
private SignalServiceAttachmentStream getAttachmentStream(
|
||||
final String attachment,
|
||||
final AttachmentDimensions dimensions,
|
||||
final String blurHash,
|
||||
final boolean voiceNote
|
||||
) throws AttachmentInvalidException {
|
||||
try {
|
||||
@ -116,6 +124,8 @@ public class AttachmentHelper {
|
||||
return AttachmentUtils.createAttachmentStream(streamDetails,
|
||||
streamDetailsAndFileName.second(),
|
||||
voiceNote,
|
||||
dimensions,
|
||||
blurHash,
|
||||
uploadSpec);
|
||||
} catch (IOException e) {
|
||||
throw new AttachmentInvalidException(attachment, e);
|
||||
@ -130,7 +140,7 @@ public class AttachmentHelper {
|
||||
}
|
||||
|
||||
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
|
||||
final var attachmentStream = getAttachmentStream(attachment, false);
|
||||
final var attachmentStream = getAttachmentStream(attachment, null, null, false);
|
||||
return uploadAttachment(attachmentStream);
|
||||
}
|
||||
|
||||
|
||||
@ -970,7 +970,10 @@ public class ManagerImpl implements Manager {
|
||||
}
|
||||
if (!message.attachments().isEmpty()) {
|
||||
final var uploadedAttachments = context.getAttachmentHelper()
|
||||
.uploadAttachments(message.attachments(), message.voiceNote());
|
||||
.uploadAttachments(message.attachments(),
|
||||
message.attachmentDimensions(),
|
||||
message.attachmentBlurHashes(),
|
||||
message.voiceNote());
|
||||
if (!additionalAttachments.isEmpty()) {
|
||||
additionalAttachments.addAll(uploadedAttachments);
|
||||
messageBuilder.withAttachments(additionalAttachments);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package org.asamk.signal.manager.util;
|
||||
|
||||
import org.asamk.signal.manager.api.Message.AttachmentDimensions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
@ -27,16 +28,21 @@ public class AttachmentUtils {
|
||||
StreamDetails streamDetails,
|
||||
Optional<String> name,
|
||||
boolean voiceNote,
|
||||
AttachmentDimensions dimensions,
|
||||
String blurHash,
|
||||
ResumableUploadSpec resumableUploadSpec
|
||||
) throws ResumeLocationInvalidException, IOException {
|
||||
final var uploadTimestamp = System.currentTimeMillis();
|
||||
final var probedStream = probeImageDimensions(streamDetails);
|
||||
final var probedStream = dimensions != null
|
||||
? new ProbedStream(streamDetails.getStream(), dimensions.width(), dimensions.height())
|
||||
: probeImageDimensions(streamDetails);
|
||||
return SignalServiceAttachmentStream.newStreamBuilder()
|
||||
.withStream(probedStream.inputStream())
|
||||
.withContentType(streamDetails.getContentType())
|
||||
.withLength(streamDetails.getLength())
|
||||
.withFileName(name.orElse(null))
|
||||
.withVoiceNote(voiceNote)
|
||||
.withBlurHash(blurHash)
|
||||
.withWidth(probedStream.width())
|
||||
.withHeight(probedStream.height())
|
||||
.withUploadTimestamp(uploadTimestamp)
|
||||
@ -50,7 +56,7 @@ public class AttachmentUtils {
|
||||
Optional<String> name,
|
||||
ResumableUploadSpec resumableUploadSpec
|
||||
) throws ResumeLocationInvalidException, IOException {
|
||||
return createAttachmentStream(streamDetails, name, false, resumableUploadSpec);
|
||||
return createAttachmentStream(streamDetails, name, false, null, null, resumableUploadSpec);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package org.asamk.signal.manager.util;
|
||||
|
||||
import org.asamk.signal.manager.api.Message.AttachmentDimensions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
|
||||
@ -43,6 +44,34 @@ class AttachmentUtilsTest {
|
||||
assertArrayEquals(bytes, attachment.getInputStream().readAllBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createAttachmentStream_setsSuppliedDimensionsAndBlurHash() throws Exception {
|
||||
final var bytes = "opaque video bytes".getBytes();
|
||||
final var blurHash = "LEHV6nWB2yk8pyo0adR*.7kCMdnj";
|
||||
final var details = new StreamDetails(new ByteArrayInputStream(bytes), "video/mp4", bytes.length);
|
||||
final var attachment = AttachmentUtils.createAttachmentStream(details,
|
||||
Optional.of("clip.mp4"), false, new AttachmentDimensions(1080, 1920), blurHash, null);
|
||||
|
||||
assertEquals(1080, attachment.getWidth());
|
||||
assertEquals(1920, attachment.getHeight());
|
||||
assertEquals(Optional.of(blurHash), attachment.getBlurHash());
|
||||
assertArrayEquals(bytes, attachment.getInputStream().readAllBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createAttachmentStream_skipsProbingWhenDimensionsSupplied() throws Exception {
|
||||
final var imageBytes = pngBytes(37, 21);
|
||||
final var stream = new ByteArrayInputStream(imageBytes);
|
||||
final var details = new StreamDetails(stream, "image/png", imageBytes.length);
|
||||
final var attachment = AttachmentUtils.createAttachmentStream(details,
|
||||
Optional.of("meme.png"), false, new AttachmentDimensions(100, 200), null, null);
|
||||
|
||||
assertEquals(imageBytes.length, stream.available());
|
||||
assertEquals(100, attachment.getWidth());
|
||||
assertEquals(200, attachment.getHeight());
|
||||
assertArrayEquals(imageBytes, attachment.getInputStream().readAllBytes());
|
||||
}
|
||||
|
||||
private static byte[] pngBytes(final int width, final int height) throws Exception {
|
||||
final var image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
final var out = new ByteArrayOutputStream();
|
||||
|
||||
@ -344,6 +344,15 @@ 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>`
|
||||
|
||||
*--attachment-dimensions* [WIDTHxHEIGHT [WIDTHxHEIGHT ...]]::
|
||||
Specify the displayed dimensions (after rotation) of the attachment at the same position in `--attachment`.
|
||||
Use an empty string to skip one; image dimensions are otherwise detected automatically.
|
||||
|
||||
*--attachment-blurhash* [BLURHASH [BLURHASH ...]]::
|
||||
Specify a BlurHash (https://blurha.sh) for the attachment at the same position in `--attachment`, which clients show while it's downloading.
|
||||
Use an empty string to skip one.
|
||||
e.g.: `--attachment a.jpg clip.mp4 --attachment-dimensions '' 1080x1920 --attachment-blurhash '' 'LEHV6nWB2yk8pyo0adR*.7kCMdnj'`
|
||||
|
||||
*--view-once*::
|
||||
Send the message as a view once message.
|
||||
A conformant client will only allow the receiver to view the message once.
|
||||
|
||||
@ -36,6 +36,7 @@ import static org.asamk.signal.util.SendMessageResultUtils.outputResult;
|
||||
public class SendCommand implements JsonRpcLocalCommand {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SendCommand.class);
|
||||
private static final String BLURHASH_DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
@ -112,6 +113,13 @@ public class SendCommand implements JsonRpcLocalCommand {
|
||||
subparser.addArgument("--voice-note")
|
||||
.action(Arguments.storeTrue())
|
||||
.help("Mark audio attachments as voice notes. Voice notes are displayed inline in Signal clients.");
|
||||
subparser.addArgument("--attachment-dimensions")
|
||||
.nargs("*")
|
||||
.help("Specify displayed WIDTHxHEIGHT for each attachment in order. Use an empty string to skip one.");
|
||||
subparser.addArgument("--attachment-blurhash")
|
||||
.nargs("*")
|
||||
.help("Specify a BlurHash (https://blurha.sh) for each attachment in order, which clients show "
|
||||
+ "while it's downloading. Use an empty string to skip one.");
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -175,6 +183,13 @@ public class SendCommand implements JsonRpcLocalCommand {
|
||||
final var viewOnce = Boolean.TRUE.equals(ns.getBoolean("view-once"));
|
||||
final var voiceNote = Boolean.TRUE.equals(ns.getBoolean("voice-note"));
|
||||
|
||||
final var dimensionStrings = ns.<String>getList("attachment-dimensions");
|
||||
final var attachmentDimensions = dimensionStrings == null
|
||||
? List.<Message.AttachmentDimensions>of()
|
||||
: parseAttachmentDimensions(dimensionStrings);
|
||||
final var blurHashes = ns.<String>getList("attachment-blurhash");
|
||||
final var attachmentBlurHashes = blurHashes == null ? List.<String>of() : parseAttachmentBlurHashes(blurHashes);
|
||||
|
||||
final var selfNumber = m.getSelfNumber();
|
||||
|
||||
final var mentionStrings = ns.<String>getList("mention");
|
||||
@ -249,6 +264,8 @@ public class SendCommand implements JsonRpcLocalCommand {
|
||||
try {
|
||||
final var message = new Message(messageText,
|
||||
attachments,
|
||||
attachmentDimensions,
|
||||
attachmentBlurHashes,
|
||||
viewOnce,
|
||||
voiceNote,
|
||||
mentions,
|
||||
@ -280,6 +297,46 @@ public class SendCommand implements JsonRpcLocalCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private List<Message.AttachmentDimensions> parseAttachmentDimensions(
|
||||
final List<String> dimensionStrings
|
||||
) throws UserErrorException {
|
||||
final var dimensionPattern = Pattern.compile("([1-9]\\d*)x([1-9]\\d*)");
|
||||
final var dimensions = new ArrayList<Message.AttachmentDimensions>();
|
||||
for (final var dimension : dimensionStrings) {
|
||||
if (dimension.isEmpty()) {
|
||||
dimensions.add(null);
|
||||
continue;
|
||||
}
|
||||
final var matcher = dimensionPattern.matcher(dimension);
|
||||
if (!matcher.matches()) {
|
||||
throw new UserErrorException("Invalid attachment dimensions syntax ("
|
||||
+ dimension
|
||||
+ ") expected 'WIDTHxHEIGHT'");
|
||||
}
|
||||
dimensions.add(new Message.AttachmentDimensions(Integer.parseInt(matcher.group(1)),
|
||||
Integer.parseInt(matcher.group(2))));
|
||||
}
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
private List<String> parseAttachmentBlurHashes(final List<String> blurHashes) throws UserErrorException {
|
||||
for (final var blurHash : blurHashes) {
|
||||
if (!blurHash.isEmpty() && !isValidBlurHash(blurHash)) {
|
||||
throw new UserErrorException("Invalid attachment BlurHash (" + blurHash + ")");
|
||||
}
|
||||
}
|
||||
return blurHashes;
|
||||
}
|
||||
|
||||
// Same check the BlurHash decoders make: the first digit fixes the length.
|
||||
private static boolean isValidBlurHash(final String blurHash) {
|
||||
if (blurHash.length() < 6) {
|
||||
return false;
|
||||
}
|
||||
final var sizeFlag = BLURHASH_DIGITS.indexOf(blurHash.charAt(0));
|
||||
return blurHash.length() == 4 + 2 * (sizeFlag % 9 + 1) * (sizeFlag / 9 + 1);
|
||||
}
|
||||
|
||||
private List<Message.Mention> parseMentions(
|
||||
final String selfNumber,
|
||||
final List<String> mentionStrings
|
||||
|
||||
@ -242,6 +242,8 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
||||
try {
|
||||
final var message = new Message(messageText,
|
||||
attachments,
|
||||
List.of(),
|
||||
List.of(),
|
||||
false,
|
||||
false,
|
||||
List.of(),
|
||||
@ -409,6 +411,8 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
||||
try {
|
||||
final var message = new Message(messageText,
|
||||
attachments,
|
||||
List.of(),
|
||||
List.of(),
|
||||
false,
|
||||
false,
|
||||
List.of(),
|
||||
@ -456,6 +460,8 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
||||
try {
|
||||
final var message = new Message(messageText,
|
||||
attachments,
|
||||
List.of(),
|
||||
List.of(),
|
||||
false,
|
||||
false,
|
||||
List.of(),
|
||||
|
||||
@ -0,0 +1,117 @@
|
||||
package org.asamk.signal.commands;
|
||||
|
||||
import net.sourceforge.argparse4j.ArgumentParsers;
|
||||
import net.sourceforge.argparse4j.DefaultSettings;
|
||||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
|
||||
import org.asamk.signal.commands.exceptions.UserErrorException;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.api.Message;
|
||||
import org.asamk.signal.manager.api.SendMessageResults;
|
||||
import org.asamk.signal.output.JsonWriter;
|
||||
import org.asamk.signal.testutil.ManagerMock;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class SendCommandParsingTest {
|
||||
|
||||
private static final List<String> ATTACHMENTS = List.of("photo.jpg", "portrait.mp4", "other.mp4");
|
||||
private static final String BLUR_HASH = "LEHV6nWB2yk8pyo0adR*.7kCMdnj";
|
||||
|
||||
@Test
|
||||
void cliPreservesEmptyAndMissingDimensionPositions() throws Exception {
|
||||
final var parser = ArgumentParsers.newFor("signal-cli", DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS)
|
||||
.includeArgumentNamesAsKeysInResult(true).build();
|
||||
new SendCommand().attachToSubparser(parser.addSubparsers().addParser("send"));
|
||||
final var message = send(parser.parseArgs(new String[]{"send", "--note-to-self",
|
||||
"-a", "photo.jpg", "portrait.mp4", "other.mp4", "--attachment-dimensions", "", "1080x1920"}));
|
||||
|
||||
assertEquals(ATTACHMENTS, message.attachments());
|
||||
assertEquals(Arrays.asList(null, new Message.AttachmentDimensions(1080, 1920)),
|
||||
message.attachmentDimensions());
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonRpcPassesDimensionsIntoTheSentMessage() throws Exception {
|
||||
final var message = send(namespace(List.of("", "1920x1080")));
|
||||
assertEquals(ATTACHMENTS, message.attachments());
|
||||
assertEquals(Arrays.asList(null, new Message.AttachmentDimensions(1920, 1080)),
|
||||
message.attachmentDimensions());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cliPreservesBlurHashPositions() throws Exception {
|
||||
final var parser = ArgumentParsers.newFor("signal-cli", DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS)
|
||||
.includeArgumentNamesAsKeysInResult(true).build();
|
||||
new SendCommand().attachToSubparser(parser.addSubparsers().addParser("send"));
|
||||
final var message = send(parser.parseArgs(new String[]{"send", "--note-to-self",
|
||||
"-a", "photo.jpg", "portrait.mp4", "other.mp4", "--attachment-blurhash", "", BLUR_HASH}));
|
||||
|
||||
assertEquals(List.of("", BLUR_HASH), message.attachmentBlurHashes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonRpcPassesBlurHashesWithDimensions() throws Exception {
|
||||
final var message = send(new JsonRpcNamespace(Map.of("noteToSelf", true, "attachments", ATTACHMENTS,
|
||||
"attachmentDimensions", List.of("", "1920x1080"), "attachmentBlurhash", List.of("", BLUR_HASH))));
|
||||
|
||||
assertEquals(Arrays.asList(null, new Message.AttachmentDimensions(1920, 1080)),
|
||||
message.attachmentDimensions());
|
||||
assertEquals(List.of("", BLUR_HASH), message.attachmentBlurHashes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void omittedDimensionsKeepExistingBehavior() throws Exception {
|
||||
final var message = send(new JsonRpcNamespace(Map.of("noteToSelf", true, "attachments", ATTACHMENTS)));
|
||||
assertEquals(List.of(), message.attachmentDimensions());
|
||||
assertEquals(List.of(), message.attachmentBlurHashes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMalformedDimensionsBeforeSending() {
|
||||
for (final var value : List.of("0x1080", "1920x0", "-1x2", "1.5x2", "1920")) {
|
||||
assertThrows(UserErrorException.class, () -> send(namespace(List.of(value))), value);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMalformedBlurHashesBeforeSending() {
|
||||
// Wrong length for its size digit, an unknown size digit, too short.
|
||||
for (final var value : List.of(BLUR_HASH + "0", "!" + BLUR_HASH.substring(1), "00000")) {
|
||||
assertThrows(UserErrorException.class, () -> send(blurHashNamespace(List.of(value))), value);
|
||||
}
|
||||
}
|
||||
|
||||
private JsonRpcNamespace blurHashNamespace(List<String> blurHashes) {
|
||||
return new JsonRpcNamespace(Map.of("noteToSelf", true, "attachments", ATTACHMENTS,
|
||||
"attachmentBlurhash", blurHashes));
|
||||
}
|
||||
|
||||
private JsonRpcNamespace namespace(List<String> dimensions) {
|
||||
return new JsonRpcNamespace(Map.of("noteToSelf", true, "attachments", ATTACHMENTS,
|
||||
"attachmentDimensions", dimensions));
|
||||
}
|
||||
|
||||
private Message send(Namespace namespace) throws Exception {
|
||||
final var captured = new AtomicReference<Message>();
|
||||
final var delegate = ManagerMock.create("+15551234567");
|
||||
final var manager = (Manager) Proxy.newProxyInstance(Manager.class.getClassLoader(),
|
||||
new Class<?>[]{Manager.class}, (proxy, method, args) -> {
|
||||
if (method.getName().equals("sendMessage")) {
|
||||
captured.set((Message) args[0]);
|
||||
return new SendMessageResults(1, Map.of());
|
||||
}
|
||||
return method.invoke(delegate, args);
|
||||
});
|
||||
new SendCommand().handleCommand(namespace, manager, (JsonWriter) ignored -> {});
|
||||
return captured.get();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user