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