]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java
Implement jsonRpc command
[signal-cli] / src / main / java / org / asamk / signal / commands / JsonRpcDispatcherCommand.java
1 package org.asamk.signal.commands;
2
3 import com.fasterxml.jackson.core.TreeNode;
4 import com.fasterxml.jackson.databind.JsonMappingException;
5 import com.fasterxml.jackson.databind.JsonNode;
6 import com.fasterxml.jackson.databind.ObjectMapper;
7 import com.fasterxml.jackson.databind.node.ContainerNode;
8
9 import net.sourceforge.argparse4j.impl.Arguments;
10 import net.sourceforge.argparse4j.inf.Namespace;
11 import net.sourceforge.argparse4j.inf.Subparser;
12
13 import org.asamk.signal.JsonReceiveMessageHandler;
14 import org.asamk.signal.JsonWriter;
15 import org.asamk.signal.OutputType;
16 import org.asamk.signal.OutputWriter;
17 import org.asamk.signal.commands.exceptions.CommandException;
18 import org.asamk.signal.commands.exceptions.IOErrorException;
19 import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
20 import org.asamk.signal.commands.exceptions.UserErrorException;
21 import org.asamk.signal.jsonrpc.JsonRpcException;
22 import org.asamk.signal.jsonrpc.JsonRpcReader;
23 import org.asamk.signal.jsonrpc.JsonRpcRequest;
24 import org.asamk.signal.jsonrpc.JsonRpcResponse;
25 import org.asamk.signal.jsonrpc.JsonRpcSender;
26 import org.asamk.signal.manager.Manager;
27 import org.asamk.signal.util.Util;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
30
31 import java.io.BufferedReader;
32 import java.io.IOException;
33 import java.io.InputStreamReader;
34 import java.util.Set;
35 import java.util.concurrent.TimeUnit;
36
37 public class JsonRpcDispatcherCommand implements LocalCommand {
38
39 private final static Logger logger = LoggerFactory.getLogger(JsonRpcDispatcherCommand.class);
40
41 private static final int USER_ERROR = -1;
42 private static final int IO_ERROR = -3;
43 private static final int UNTRUSTED_KEY_ERROR = -4;
44
45 private final OutputWriter outputWriter;
46
47 public static void attachToSubparser(final Subparser subparser) {
48 subparser.help("Take commands from standard input as line-delimited JSON RPC while receiving messages.");
49 subparser.addArgument("--ignore-attachments")
50 .help("Don’t download attachments of received messages.")
51 .action(Arguments.storeTrue());
52 }
53
54 public JsonRpcDispatcherCommand(final OutputWriter outputWriter) {
55 this.outputWriter = outputWriter;
56 }
57
58 @Override
59 public Set<OutputType> getSupportedOutputTypes() {
60 return Set.of(OutputType.JSON);
61 }
62
63 @Override
64 public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
65 final boolean ignoreAttachments = ns.getBoolean("ignore-attachments");
66
67 final var objectMapper = Util.createJsonObjectMapper();
68 final var jsonRpcSender = new JsonRpcSender((JsonWriter) outputWriter);
69
70 final var receiveThread = receiveMessages(s -> jsonRpcSender.sendRequest(JsonRpcRequest.forNotification(
71 "receive",
72 objectMapper.valueToTree(s),
73 null)), m, ignoreAttachments);
74
75 final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
76
77 final var jsonRpcReader = new JsonRpcReader(jsonRpcSender, () -> {
78 try {
79 return reader.readLine();
80 } catch (IOException e) {
81 throw new AssertionError(e);
82 }
83 });
84 jsonRpcReader.readRequests((method, params) -> handleRequest(m, objectMapper, method, params),
85 response -> logger.debug("Received unexpected response for id {}", response.getId()));
86
87 receiveThread.interrupt();
88 try {
89 receiveThread.join();
90 } catch (InterruptedException ignored) {
91 }
92 }
93
94 private JsonNode handleRequest(
95 final Manager m, final ObjectMapper objectMapper, final String method, ContainerNode<?> params
96 ) throws JsonRpcException {
97 final Object[] result = {null};
98 final JsonWriter commandOutputWriter = s -> {
99 if (result[0] != null) {
100 throw new AssertionError("Command may only write one json result");
101 }
102
103 result[0] = s;
104 };
105
106 var command = Commands.getCommand(method, commandOutputWriter);
107 if (!(command instanceof JsonRpcCommand)) {
108 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.METHOD_NOT_FOUND,
109 "Method not implemented",
110 null));
111 }
112
113 try {
114 parseParamsAndRunCommand(m, objectMapper, params, (JsonRpcCommand<?>) command);
115 } catch (JsonMappingException e) {
116 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
117 e.getMessage(),
118 null));
119 } catch (UserErrorException e) {
120 throw new JsonRpcException(new JsonRpcResponse.Error(USER_ERROR, e.getMessage(), null));
121 } catch (IOErrorException e) {
122 throw new JsonRpcException(new JsonRpcResponse.Error(IO_ERROR, e.getMessage(), null));
123 } catch (UntrustedKeyErrorException e) {
124 throw new JsonRpcException(new JsonRpcResponse.Error(UNTRUSTED_KEY_ERROR, e.getMessage(), null));
125 } catch (Throwable e) {
126 logger.error("Command execution failed", e);
127 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INTERNAL_ERROR,
128 e.getMessage(),
129 null));
130 }
131
132 Object output = result[0] == null ? new Object() : result[0];
133 return objectMapper.valueToTree(output);
134 }
135
136 private <T> void parseParamsAndRunCommand(
137 final Manager m, final ObjectMapper objectMapper, final TreeNode params, final JsonRpcCommand<T> command
138 ) throws CommandException, JsonMappingException {
139 T requestParams = null;
140 final var requestType = command.getRequestType();
141 if (params != null && requestType != null) {
142 try {
143 requestParams = objectMapper.readValue(objectMapper.treeAsTokens(params), requestType);
144 } catch (JsonMappingException e) {
145 throw e;
146 } catch (IOException e) {
147 throw new AssertionError(e);
148 }
149 }
150 command.handleCommand(requestParams, m);
151 }
152
153 private Thread receiveMessages(
154 JsonWriter jsonWriter, Manager m, boolean ignoreAttachments
155 ) {
156 final var thread = new Thread(() -> {
157 while (!Thread.interrupted()) {
158 try {
159 final var receiveMessageHandler = new JsonReceiveMessageHandler(m, jsonWriter);
160 m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, receiveMessageHandler);
161 break;
162 } catch (IOException e) {
163 logger.warn("Receiving messages failed, retrying", e);
164 } catch (InterruptedException e) {
165 break;
166 }
167 }
168 });
169
170 thread.start();
171
172 return thread;
173 }
174 }