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