]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/jsonrpc/SignalJsonRpcDispatcherHandler.java
Add listAccounts command
[signal-cli] / src / main / java / org / asamk / signal / jsonrpc / SignalJsonRpcDispatcherHandler.java
1 package org.asamk.signal.jsonrpc;
2
3 import com.fasterxml.jackson.core.TreeNode;
4 import com.fasterxml.jackson.core.type.TypeReference;
5 import com.fasterxml.jackson.databind.JsonMappingException;
6 import com.fasterxml.jackson.databind.JsonNode;
7 import com.fasterxml.jackson.databind.ObjectMapper;
8 import com.fasterxml.jackson.databind.node.ContainerNode;
9
10 import org.asamk.signal.JsonReceiveMessageHandler;
11 import org.asamk.signal.JsonWriter;
12 import org.asamk.signal.commands.Command;
13 import org.asamk.signal.commands.Commands;
14 import org.asamk.signal.commands.JsonRpcMultiCommand;
15 import org.asamk.signal.commands.JsonRpcSingleCommand;
16 import org.asamk.signal.commands.SignalCreator;
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.manager.Manager;
22 import org.asamk.signal.util.Util;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25
26 import java.io.IOException;
27 import java.util.HashMap;
28 import java.util.Map;
29 import java.util.Objects;
30 import java.util.function.Supplier;
31
32 public class SignalJsonRpcDispatcherHandler {
33
34 private final static Logger logger = LoggerFactory.getLogger(SignalJsonRpcDispatcherHandler.class);
35
36 private static final int USER_ERROR = -1;
37 private static final int IO_ERROR = -3;
38 private static final int UNTRUSTED_KEY_ERROR = -4;
39
40 private final ObjectMapper objectMapper;
41 private final JsonRpcSender jsonRpcSender;
42 private final JsonRpcReader jsonRpcReader;
43 private final boolean noReceiveOnStart;
44
45 private SignalCreator c;
46 private final Map<Manager, Manager.ReceiveMessageHandler> receiveHandlers = new HashMap<>();
47
48 private Manager m;
49
50 public SignalJsonRpcDispatcherHandler(
51 final JsonWriter jsonWriter, final Supplier<String> lineSupplier, final boolean noReceiveOnStart
52 ) {
53 this.noReceiveOnStart = noReceiveOnStart;
54 this.objectMapper = Util.createJsonObjectMapper();
55 this.jsonRpcSender = new JsonRpcSender(jsonWriter);
56 this.jsonRpcReader = new JsonRpcReader(jsonRpcSender, lineSupplier);
57 }
58
59 public void handleConnection(final SignalCreator c) {
60 this.c = c;
61
62 if (!noReceiveOnStart) {
63 c.getAccountNumbers().stream().map(c::getManager).filter(Objects::nonNull).forEach(this::subscribeReceive);
64 }
65
66 handleConnection();
67 }
68
69 public void handleConnection(final Manager m) {
70 this.m = m;
71
72 if (!noReceiveOnStart) {
73 subscribeReceive(m);
74 }
75
76 handleConnection();
77 }
78
79 private void subscribeReceive(final Manager m) {
80 if (receiveHandlers.containsKey(m)) {
81 return;
82 }
83
84 final var receiveMessageHandler = new JsonReceiveMessageHandler(m,
85 s -> jsonRpcSender.sendRequest(JsonRpcRequest.forNotification("receive",
86 objectMapper.valueToTree(s),
87 null)));
88 m.addReceiveHandler(receiveMessageHandler);
89 receiveHandlers.put(m, receiveMessageHandler);
90
91 while (!m.hasCaughtUpWithOldMessages()) {
92 try {
93 synchronized (m) {
94 m.wait();
95 }
96 } catch (InterruptedException ignored) {
97 }
98 }
99 }
100
101 void unsubscribeReceive(final Manager m) {
102 final var receiveMessageHandler = receiveHandlers.remove(m);
103 if (receiveMessageHandler != null) {
104 m.removeReceiveHandler(receiveMessageHandler);
105 }
106 }
107
108 private void handleConnection() {
109 try {
110 jsonRpcReader.readMessages((method, params) -> handleRequest(objectMapper, method, params),
111 response -> logger.debug("Received unexpected response for id {}", response.getId()));
112 } finally {
113 receiveHandlers.forEach(Manager::removeReceiveHandler);
114 receiveHandlers.clear();
115 }
116 }
117
118 private JsonNode handleRequest(
119 final ObjectMapper objectMapper, final String method, ContainerNode<?> params
120 ) throws JsonRpcException {
121 var command = getCommand(method);
122 // TODO implement register, verify, link
123 if (c != null && command instanceof JsonRpcMultiCommand<?> jsonRpcCommand) {
124 return runCommand(objectMapper, params, new MultiCommandRunnerImpl<>(c, jsonRpcCommand));
125 }
126 if (command instanceof JsonRpcSingleCommand<?> jsonRpcCommand) {
127 if (m != null) {
128 return runCommand(objectMapper, params, new CommandRunnerImpl<>(m, jsonRpcCommand));
129 }
130
131 if (params.has("account")) {
132 Manager manager = c.getManager(params.get("account").asText());
133 if (manager != null) {
134 return runCommand(objectMapper, params, new CommandRunnerImpl<>(manager, jsonRpcCommand));
135 }
136 } else {
137 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_PARAMS,
138 "Method requires valid account parameter",
139 null));
140 }
141 }
142
143 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.METHOD_NOT_FOUND,
144 "Method not implemented",
145 null));
146 }
147
148 private Command getCommand(final String method) {
149 if ("subscribeReceive".equals(method)) {
150 return new SubscribeReceiveCommand();
151 }
152 if ("unsubscribeReceive".equals(method)) {
153 return new UnsubscribeReceiveCommand();
154 }
155 return Commands.getCommand(method);
156 }
157
158 private record CommandRunnerImpl<T>(Manager m, JsonRpcSingleCommand<T> command) implements CommandRunner<T> {
159
160 @Override
161 public void handleCommand(final T request, final JsonWriter jsonWriter) throws CommandException {
162 command.handleCommand(request, m, jsonWriter);
163 }
164
165 @Override
166 public TypeReference<T> getRequestType() {
167 return command.getRequestType();
168 }
169 }
170
171 private record MultiCommandRunnerImpl<T>(
172 SignalCreator c, JsonRpcMultiCommand<T> command
173 ) implements CommandRunner<T> {
174
175 @Override
176 public void handleCommand(final T request, final JsonWriter jsonWriter) throws CommandException {
177 command.handleCommand(request, c, jsonWriter);
178 }
179
180 @Override
181 public TypeReference<T> getRequestType() {
182 return command.getRequestType();
183 }
184 }
185
186 interface CommandRunner<T> {
187
188 void handleCommand(T request, JsonWriter jsonWriter) throws CommandException;
189
190 TypeReference<T> getRequestType();
191 }
192
193 private JsonNode runCommand(
194 final ObjectMapper objectMapper, final ContainerNode<?> params, final CommandRunner<?> command
195 ) throws JsonRpcException {
196 final Object[] result = {null};
197 final JsonWriter commandJsonWriter = s -> {
198 if (result[0] != null) {
199 throw new AssertionError("Command may only write one json result");
200 }
201
202 result[0] = s;
203 };
204
205 try {
206 parseParamsAndRunCommand(objectMapper, params, commandJsonWriter, command);
207 } catch (JsonMappingException e) {
208 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
209 e.getMessage(),
210 null));
211 } catch (UserErrorException e) {
212 throw new JsonRpcException(new JsonRpcResponse.Error(USER_ERROR, e.getMessage(), null));
213 } catch (IOErrorException e) {
214 throw new JsonRpcException(new JsonRpcResponse.Error(IO_ERROR, e.getMessage(), null));
215 } catch (UntrustedKeyErrorException e) {
216 throw new JsonRpcException(new JsonRpcResponse.Error(UNTRUSTED_KEY_ERROR, e.getMessage(), null));
217 } catch (Throwable e) {
218 logger.error("Command execution failed", e);
219 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INTERNAL_ERROR,
220 e.getMessage(),
221 null));
222 }
223
224 Object output = result[0] == null ? Map.of() : result[0];
225 return objectMapper.valueToTree(output);
226 }
227
228 private <T> void parseParamsAndRunCommand(
229 final ObjectMapper objectMapper,
230 final TreeNode params,
231 final JsonWriter jsonWriter,
232 final CommandRunner<T> command
233 ) throws CommandException, JsonMappingException {
234 T requestParams = null;
235 final var requestType = command.getRequestType();
236 if (params != null && requestType != null) {
237 try {
238 requestParams = objectMapper.readValue(objectMapper.treeAsTokens(params), requestType);
239 } catch (JsonMappingException e) {
240 throw e;
241 } catch (IOException e) {
242 throw new AssertionError(e);
243 }
244 }
245 command.handleCommand(requestParams, jsonWriter);
246 }
247
248 private class SubscribeReceiveCommand implements JsonRpcSingleCommand<Void> {
249
250 @Override
251 public String getName() {
252 return "subscribeReceive";
253 }
254
255 @Override
256 public void handleCommand(
257 final Void request, final Manager m, final JsonWriter jsonWriter
258 ) throws CommandException {
259 subscribeReceive(m);
260 }
261 }
262
263 private class UnsubscribeReceiveCommand implements JsonRpcSingleCommand<Void> {
264
265 @Override
266 public String getName() {
267 return "unsubscribeReceive";
268 }
269
270 @Override
271 public void handleCommand(
272 final Void request, final Manager m, final JsonWriter jsonWriter
273 ) throws CommandException {
274 unsubscribeReceive(m);
275 }
276 }
277 }