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