Implement jsonRpc command

Co-authored-by: technillogue <technillogue@gmail.com>

Closes #668
This commit is contained in:
AsamK 2021-08-09 17:42:01 +02:00
parent 6c00054407
commit a8bbdb54d0
20 changed files with 863 additions and 31 deletions

View file

@ -16,6 +16,7 @@ public class Commands {
addCommand("block", BlockCommand::new, BlockCommand::attachToSubparser);
addCommand("daemon", DaemonCommand::new, DaemonCommand::attachToSubparser);
addCommand("getUserStatus", GetUserStatusCommand::new, GetUserStatusCommand::attachToSubparser);
addCommand("jsonRpc", JsonRpcDispatcherCommand::new, JsonRpcDispatcherCommand::attachToSubparser);
addCommand("link", LinkCommand::new, LinkCommand::attachToSubparser);
addCommand("listContacts", ListContactsCommand::new, ListContactsCommand::attachToSubparser);
addCommand("listDevices", ListDevicesCommand::new, ListDevicesCommand::attachToSubparser);
@ -43,6 +44,7 @@ public class Commands {
addCommand("updateProfile", UpdateProfileCommand::new, UpdateProfileCommand::attachToSubparser);
addCommand("uploadStickerPack", UploadStickerPackCommand::new, UploadStickerPackCommand::attachToSubparser);
addCommand("verify", VerifyCommand::new, VerifyCommand::attachToSubparser);
addCommand("version", VersionCommand::new, null);
}
public static Map<String, SubparserAttacher> getCommandSubparserAttachers() {
@ -60,7 +62,9 @@ public class Commands {
String name, CommandConstructor commandConstructor, SubparserAttacher subparserAttacher
) {
commands.put(name, commandConstructor);
commandSubparserAttacher.put(name, subparserAttacher);
if (subparserAttacher != null) {
commandSubparserAttacher.put(name, subparserAttacher);
}
}
private interface CommandConstructor {

View file

@ -123,14 +123,17 @@ public class DaemonCommand implements MultiLocalCommand {
logger.info("Exported dbus object: " + objectPath);
final var thread = new Thread(() -> {
while (true) {
while (!Thread.interrupted()) {
try {
final var receiveMessageHandler = outputWriter instanceof JsonWriter
? new JsonDbusReceiveMessageHandler(m, (JsonWriter) outputWriter, conn, objectPath)
: new DbusReceiveMessageHandler(m, (PlainTextWriter) outputWriter, conn, objectPath);
m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, receiveMessageHandler);
break;
} catch (IOException e) {
logger.warn("Receiving messages failed, retrying", e);
} catch (InterruptedException ignored) {
break;
}
}
});

View file

@ -4,7 +4,6 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputType;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.commands.exceptions.CommandException;
@ -16,10 +15,9 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class GetUserStatusCommand implements LocalCommand {
public class GetUserStatusCommand implements JsonRpcLocalCommand {
private final static Logger logger = LoggerFactory.getLogger(GetUserStatusCommand.class);
private final OutputWriter outputWriter;
@ -33,11 +31,6 @@ public class GetUserStatusCommand implements LocalCommand {
this.outputWriter = outputWriter;
}
@Override
public Set<OutputType> getSupportedOutputTypes() {
return Set.of(OutputType.PLAIN_TEXT, OutputType.JSON);
}
@Override
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
// Get a map of registration statuses

View file

@ -0,0 +1,22 @@
package org.asamk.signal.commands;
import com.fasterxml.jackson.core.type.TypeReference;
import org.asamk.signal.OutputType;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager;
import java.util.Set;
public interface JsonRpcCommand<T> extends Command {
default TypeReference<T> getRequestType() {
return null;
}
void handleCommand(T request, Manager m) throws CommandException;
default Set<OutputType> getSupportedOutputTypes() {
return Set.of(OutputType.JSON);
}
}

View file

@ -0,0 +1,174 @@
package org.asamk.signal.commands;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ContainerNode;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.JsonReceiveMessageHandler;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputType;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.jsonrpc.JsonRpcException;
import org.asamk.signal.jsonrpc.JsonRpcReader;
import org.asamk.signal.jsonrpc.JsonRpcRequest;
import org.asamk.signal.jsonrpc.JsonRpcResponse;
import org.asamk.signal.jsonrpc.JsonRpcSender;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class JsonRpcDispatcherCommand implements LocalCommand {
private final static Logger logger = LoggerFactory.getLogger(JsonRpcDispatcherCommand.class);
private static final int USER_ERROR = -1;
private static final int IO_ERROR = -3;
private static final int UNTRUSTED_KEY_ERROR = -4;
private final OutputWriter outputWriter;
public static void attachToSubparser(final Subparser subparser) {
subparser.help("Take commands from standard input as line-delimited JSON RPC while receiving messages.");
subparser.addArgument("--ignore-attachments")
.help("Dont download attachments of received messages.")
.action(Arguments.storeTrue());
}
public JsonRpcDispatcherCommand(final OutputWriter outputWriter) {
this.outputWriter = outputWriter;
}
@Override
public Set<OutputType> getSupportedOutputTypes() {
return Set.of(OutputType.JSON);
}
@Override
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
final boolean ignoreAttachments = ns.getBoolean("ignore-attachments");
final var objectMapper = Util.createJsonObjectMapper();
final var jsonRpcSender = new JsonRpcSender((JsonWriter) outputWriter);
final var receiveThread = receiveMessages(s -> jsonRpcSender.sendRequest(JsonRpcRequest.forNotification(
"receive",
objectMapper.valueToTree(s),
null)), m, ignoreAttachments);
final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
final var jsonRpcReader = new JsonRpcReader(jsonRpcSender, () -> {
try {
return reader.readLine();
} catch (IOException e) {
throw new AssertionError(e);
}
});
jsonRpcReader.readRequests((method, params) -> handleRequest(m, objectMapper, method, params),
response -> logger.debug("Received unexpected response for id {}", response.getId()));
receiveThread.interrupt();
try {
receiveThread.join();
} catch (InterruptedException ignored) {
}
}
private JsonNode handleRequest(
final Manager m, final ObjectMapper objectMapper, final String method, ContainerNode<?> params
) throws JsonRpcException {
final Object[] result = {null};
final JsonWriter commandOutputWriter = s -> {
if (result[0] != null) {
throw new AssertionError("Command may only write one json result");
}
result[0] = s;
};
var command = Commands.getCommand(method, commandOutputWriter);
if (!(command instanceof JsonRpcCommand)) {
throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.METHOD_NOT_FOUND,
"Method not implemented",
null));
}
try {
parseParamsAndRunCommand(m, objectMapper, params, (JsonRpcCommand<?>) command);
} catch (JsonMappingException e) {
throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
e.getMessage(),
null));
} catch (UserErrorException e) {
throw new JsonRpcException(new JsonRpcResponse.Error(USER_ERROR, e.getMessage(), null));
} catch (IOErrorException e) {
throw new JsonRpcException(new JsonRpcResponse.Error(IO_ERROR, e.getMessage(), null));
} catch (UntrustedKeyErrorException e) {
throw new JsonRpcException(new JsonRpcResponse.Error(UNTRUSTED_KEY_ERROR, e.getMessage(), null));
} catch (Throwable e) {
logger.error("Command execution failed", e);
throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INTERNAL_ERROR,
e.getMessage(),
null));
}
Object output = result[0] == null ? new Object() : result[0];
return objectMapper.valueToTree(output);
}
private <T> void parseParamsAndRunCommand(
final Manager m, final ObjectMapper objectMapper, final TreeNode params, final JsonRpcCommand<T> command
) throws CommandException, JsonMappingException {
T requestParams = null;
final var requestType = command.getRequestType();
if (params != null && requestType != null) {
try {
requestParams = objectMapper.readValue(objectMapper.treeAsTokens(params), requestType);
} catch (JsonMappingException e) {
throw e;
} catch (IOException e) {
throw new AssertionError(e);
}
}
command.handleCommand(requestParams, m);
}
private Thread receiveMessages(
JsonWriter jsonWriter, Manager m, boolean ignoreAttachments
) {
final var thread = new Thread(() -> {
while (!Thread.interrupted()) {
try {
final var receiveMessageHandler = new JsonReceiveMessageHandler(m, jsonWriter);
m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, receiveMessageHandler);
break;
} catch (IOException e) {
logger.warn("Receiving messages failed, retrying", e);
} catch (InterruptedException e) {
break;
}
}
});
thread.start();
return thread;
}
}

View file

@ -0,0 +1,72 @@
package org.asamk.signal.commands;
import com.fasterxml.jackson.core.type.TypeReference;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.signal.OutputType;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.Util;
import java.util.List;
import java.util.Map;
import java.util.Set;
public interface JsonRpcLocalCommand extends JsonRpcCommand<Map<String, Object>> {
void handleCommand(Namespace ns, Manager m) throws CommandException;
default TypeReference<Map<String, Object>> getRequestType() {
return new TypeReference<>() {
};
}
default void handleCommand(Map<String, Object> request, Manager m) throws CommandException {
Namespace commandNamespace = new JsonRpcNamespace(request == null ? Map.of() : request);
handleCommand(commandNamespace, m);
}
default Set<OutputType> getSupportedOutputTypes() {
return Set.of(OutputType.PLAIN_TEXT, OutputType.JSON);
}
/**
* Namepace implementation, that defaults booleans to false and converts camel case keys to dashed strings
*/
final class JsonRpcNamespace extends Namespace {
public JsonRpcNamespace(final Map<String, Object> attrs) {
super(attrs);
}
public <T> T get(String dest) {
final T value = super.get(dest);
if (value != null) {
return value;
}
final var camelCaseString = Util.dashSeparatedToCamelCaseString(dest);
return super.get(camelCaseString);
}
@Override
public <E> List<E> getList(final String dest) {
final List<E> value = super.getList(dest);
if (value != null) {
return value;
}
return super.getList(dest + "s");
}
@Override
public Boolean getBoolean(String dest) {
Boolean maybeGotten = this.get(dest);
if (maybeGotten == null) {
maybeGotten = false;
}
return maybeGotten;
}
}
}

View file

@ -5,7 +5,6 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputType;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.commands.exceptions.CommandException;
@ -19,7 +18,7 @@ import org.slf4j.LoggerFactory;
import java.util.Set;
import java.util.stream.Collectors;
public class ListGroupsCommand implements LocalCommand {
public class ListGroupsCommand implements JsonRpcLocalCommand {
private final static Logger logger = LoggerFactory.getLogger(ListGroupsCommand.class);
@ -70,11 +69,6 @@ public class ListGroupsCommand implements LocalCommand {
this.outputWriter = outputWriter;
}
@Override
public Set<OutputType> getSupportedOutputTypes() {
return Set.of(OutputType.PLAIN_TEXT, OutputType.JSON);
}
@Override
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
final var groups = m.getGroups();

View file

@ -155,6 +155,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
handler);
} catch (IOException e) {
throw new IOErrorException("Error while receiving messages: " + e.getMessage());
} catch (InterruptedException ignored) {
}
}
}

View file

@ -5,12 +5,15 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
@ -22,8 +25,9 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
public class SendCommand implements DbusCommand {
public class SendCommand implements DbusCommand, JsonRpcLocalCommand {
private final static Logger logger = LoggerFactory.getLogger(SendCommand.class);
private final OutputWriter outputWriter;
@ -92,8 +96,6 @@ public class SendCommand implements DbusCommand {
attachments = List.of();
}
final var writer = (PlainTextWriterImpl) outputWriter;
if (groupIdString != null) {
byte[] groupId;
try {
@ -104,7 +106,7 @@ public class SendCommand implements DbusCommand {
try {
var timestamp = signal.sendGroupMessage(messageText, attachments, groupId);
writer.println("{}", timestamp);
outputResult(timestamp);
return;
} catch (DBusExecutionException e) {
throw new UnexpectedErrorException("Failed to send group message: " + e.getMessage());
@ -114,7 +116,7 @@ public class SendCommand implements DbusCommand {
if (isNoteToSelf) {
try {
var timestamp = signal.sendNoteToSelfMessage(messageText, attachments);
writer.println("{}", timestamp);
outputResult(timestamp);
return;
} catch (Signal.Error.UntrustedIdentity e) {
throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage());
@ -125,7 +127,7 @@ public class SendCommand implements DbusCommand {
try {
var timestamp = signal.sendMessage(messageText, attachments, recipients);
writer.println("{}", timestamp);
outputResult(timestamp);
} catch (UnknownObject e) {
throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage());
} catch (Signal.Error.UntrustedIdentity e) {
@ -134,4 +136,19 @@ public class SendCommand implements DbusCommand {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage());
}
}
private void outputResult(final long timestamp) {
if (outputWriter instanceof PlainTextWriter) {
final var writer = (PlainTextWriter) outputWriter;
writer.println("{}", timestamp);
} else {
final var writer = (JsonWriter) outputWriter;
writer.write(Map.of("timestamp", timestamp));
}
}
@Override
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
handleCommand(ns, new DbusSignalImpl(m, null));
}
}

View file

@ -0,0 +1,24 @@
package org.asamk.signal.commands;
import org.asamk.signal.BaseConfig;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager;
import java.util.Map;
public class VersionCommand implements JsonRpcCommand<Void> {
private final OutputWriter outputWriter;
public VersionCommand(final OutputWriter outputWriter) {
this.outputWriter = outputWriter;
}
@Override
public void handleCommand(final Void request, final Manager m) throws CommandException {
final var jsonWriter = (JsonWriter) outputWriter;
jsonWriter.write(Map.of("version", BaseConfig.PROJECT_VERSION));
}
}