Implement jsonRpc command
Co-authored-by: technillogue <technillogue@gmail.com> Closes #668
This commit is contained in:
		
							parent
							
								
									6c00054407
								
							
						
					
					
						commit
						a8bbdb54d0
					
				| @ -2028,6 +2028,9 @@ public class Manager implements Closeable { | ||||
|             try { | ||||
|                 action.execute(this); | ||||
|             } catch (Throwable e) { | ||||
|                 if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { | ||||
|                     Thread.currentThread().interrupt(); | ||||
|                 } | ||||
|                 logger.warn("Message action failed.", e); | ||||
|             } | ||||
|         } | ||||
| @ -2074,7 +2077,7 @@ public class Manager implements Closeable { | ||||
|             boolean returnOnTimeout, | ||||
|             boolean ignoreAttachments, | ||||
|             ReceiveMessageHandler handler | ||||
|     ) throws IOException { | ||||
|     ) throws IOException, InterruptedException { | ||||
|         retryFailedReceivedMessages(handler, ignoreAttachments); | ||||
| 
 | ||||
|         Set<HandleAction> queuedActions = null; | ||||
| @ -2110,6 +2113,9 @@ public class Manager implements Closeable { | ||||
|                             try { | ||||
|                                 action.execute(this); | ||||
|                             } catch (Throwable e) { | ||||
|                                 if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { | ||||
|                                     Thread.currentThread().interrupt(); | ||||
|                                 } | ||||
|                                 logger.warn("Message action failed.", e); | ||||
|                             } | ||||
|                         } | ||||
| @ -2120,6 +2126,12 @@ public class Manager implements Closeable { | ||||
|                     // Continue to wait another timeout for new messages | ||||
|                     continue; | ||||
|                 } | ||||
|             } catch (AssertionError e) { | ||||
|                 if (e.getCause() instanceof InterruptedException) { | ||||
|                     throw (InterruptedException) e.getCause(); | ||||
|                 } else { | ||||
|                     throw e; | ||||
|                 } | ||||
|             } catch (TimeoutException e) { | ||||
|                 if (returnOnTimeout) return; | ||||
|                 continue; | ||||
| @ -2153,6 +2165,9 @@ public class Manager implements Closeable { | ||||
|                         try { | ||||
|                             action.execute(this); | ||||
|                         } catch (Throwable e) { | ||||
|                             if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { | ||||
|                                 Thread.currentThread().interrupt(); | ||||
|                             } | ||||
|                             logger.warn("Message action failed.", e); | ||||
|                         } | ||||
|                     } | ||||
| @ -2549,6 +2564,9 @@ public class Manager implements Closeable { | ||||
|             avatarStore.storeProfileAvatar(address, | ||||
|                     outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream)); | ||||
|         } catch (Throwable e) { | ||||
|             if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { | ||||
|                 Thread.currentThread().interrupt(); | ||||
|             } | ||||
|             logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -1,11 +1,10 @@ | ||||
| package org.asamk.signal; | ||||
| 
 | ||||
| import com.fasterxml.jackson.annotation.JsonAutoDetect; | ||||
| import com.fasterxml.jackson.annotation.PropertyAccessor; | ||||
| import com.fasterxml.jackson.core.JsonGenerator; | ||||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||
| 
 | ||||
| import org.asamk.signal.util.Util; | ||||
| 
 | ||||
| import java.io.BufferedWriter; | ||||
| import java.io.IOException; | ||||
| import java.io.OutputStream; | ||||
| @ -21,9 +20,7 @@ public class JsonWriterImpl implements JsonWriter { | ||||
|     public JsonWriterImpl(final OutputStream outputStream) { | ||||
|         this.writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); | ||||
| 
 | ||||
|         objectMapper = new ObjectMapper(); | ||||
|         objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY); | ||||
|         objectMapper.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); | ||||
|         objectMapper = Util.createJsonObjectMapper(); | ||||
|     } | ||||
| 
 | ||||
|     public synchronized void write(final Object object) { | ||||
|  | ||||
| @ -16,6 +16,7 @@ public class Commands { | ||||
|         addCommand("block", BlockCommand::new, BlockCommand::attachToSubparser); | ||||
|         addCommand("daemon", DaemonCommand::new, DaemonCommand::attachToSubparser); | ||||
|         addCommand("getUserStatus", GetUserStatusCommand::new, GetUserStatusCommand::attachToSubparser); | ||||
|         addCommand("jsonRpc", JsonRpcDispatcherCommand::new, JsonRpcDispatcherCommand::attachToSubparser); | ||||
|         addCommand("link", LinkCommand::new, LinkCommand::attachToSubparser); | ||||
|         addCommand("listContacts", ListContactsCommand::new, ListContactsCommand::attachToSubparser); | ||||
|         addCommand("listDevices", ListDevicesCommand::new, ListDevicesCommand::attachToSubparser); | ||||
| @ -43,6 +44,7 @@ public class Commands { | ||||
|         addCommand("updateProfile", UpdateProfileCommand::new, UpdateProfileCommand::attachToSubparser); | ||||
|         addCommand("uploadStickerPack", UploadStickerPackCommand::new, UploadStickerPackCommand::attachToSubparser); | ||||
|         addCommand("verify", VerifyCommand::new, VerifyCommand::attachToSubparser); | ||||
|         addCommand("version", VersionCommand::new, null); | ||||
|     } | ||||
| 
 | ||||
|     public static Map<String, SubparserAttacher> getCommandSubparserAttachers() { | ||||
| @ -60,7 +62,9 @@ public class Commands { | ||||
|             String name, CommandConstructor commandConstructor, SubparserAttacher subparserAttacher | ||||
|     ) { | ||||
|         commands.put(name, commandConstructor); | ||||
|         commandSubparserAttacher.put(name, subparserAttacher); | ||||
|         if (subparserAttacher != null) { | ||||
|             commandSubparserAttacher.put(name, subparserAttacher); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private interface CommandConstructor { | ||||
|  | ||||
| @ -123,14 +123,17 @@ public class DaemonCommand implements MultiLocalCommand { | ||||
|         logger.info("Exported dbus object: " + objectPath); | ||||
| 
 | ||||
|         final var thread = new Thread(() -> { | ||||
|             while (true) { | ||||
|             while (!Thread.interrupted()) { | ||||
|                 try { | ||||
|                     final var receiveMessageHandler = outputWriter instanceof JsonWriter | ||||
|                             ? new JsonDbusReceiveMessageHandler(m, (JsonWriter) outputWriter, conn, objectPath) | ||||
|                             : new DbusReceiveMessageHandler(m, (PlainTextWriter) outputWriter, conn, objectPath); | ||||
|                     m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, receiveMessageHandler); | ||||
|                     break; | ||||
|                 } catch (IOException e) { | ||||
|                     logger.warn("Receiving messages failed, retrying", e); | ||||
|                 } catch (InterruptedException ignored) { | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
| @ -4,7 +4,6 @@ import net.sourceforge.argparse4j.inf.Namespace; | ||||
| import net.sourceforge.argparse4j.inf.Subparser; | ||||
| 
 | ||||
| import org.asamk.signal.JsonWriter; | ||||
| import org.asamk.signal.OutputType; | ||||
| import org.asamk.signal.OutputWriter; | ||||
| import org.asamk.signal.PlainTextWriter; | ||||
| import org.asamk.signal.commands.exceptions.CommandException; | ||||
| @ -16,10 +15,9 @@ import org.slf4j.LoggerFactory; | ||||
| import java.io.IOException; | ||||
| import java.util.HashSet; | ||||
| import java.util.Map; | ||||
| import java.util.Set; | ||||
| import java.util.stream.Collectors; | ||||
| 
 | ||||
| public class GetUserStatusCommand implements LocalCommand { | ||||
| public class GetUserStatusCommand implements JsonRpcLocalCommand { | ||||
| 
 | ||||
|     private final static Logger logger = LoggerFactory.getLogger(GetUserStatusCommand.class); | ||||
|     private final OutputWriter outputWriter; | ||||
| @ -33,11 +31,6 @@ public class GetUserStatusCommand implements LocalCommand { | ||||
|         this.outputWriter = outputWriter; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Set<OutputType> getSupportedOutputTypes() { | ||||
|         return Set.of(OutputType.PLAIN_TEXT, OutputType.JSON); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleCommand(final Namespace ns, final Manager m) throws CommandException { | ||||
|         // Get a map of registration statuses | ||||
|  | ||||
							
								
								
									
										22
									
								
								src/main/java/org/asamk/signal/commands/JsonRpcCommand.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/main/java/org/asamk/signal/commands/JsonRpcCommand.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| package org.asamk.signal.commands; | ||||
| 
 | ||||
| import com.fasterxml.jackson.core.type.TypeReference; | ||||
| 
 | ||||
| import org.asamk.signal.OutputType; | ||||
| import org.asamk.signal.commands.exceptions.CommandException; | ||||
| import org.asamk.signal.manager.Manager; | ||||
| 
 | ||||
| import java.util.Set; | ||||
| 
 | ||||
| public interface JsonRpcCommand<T> extends Command { | ||||
| 
 | ||||
|     default TypeReference<T> getRequestType() { | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     void handleCommand(T request, Manager m) throws CommandException; | ||||
| 
 | ||||
|     default Set<OutputType> getSupportedOutputTypes() { | ||||
|         return Set.of(OutputType.JSON); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,174 @@ | ||||
| package org.asamk.signal.commands; | ||||
| 
 | ||||
| import com.fasterxml.jackson.core.TreeNode; | ||||
| import com.fasterxml.jackson.databind.JsonMappingException; | ||||
| import com.fasterxml.jackson.databind.JsonNode; | ||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||
| import com.fasterxml.jackson.databind.node.ContainerNode; | ||||
| 
 | ||||
| import net.sourceforge.argparse4j.impl.Arguments; | ||||
| import net.sourceforge.argparse4j.inf.Namespace; | ||||
| import net.sourceforge.argparse4j.inf.Subparser; | ||||
| 
 | ||||
| import org.asamk.signal.JsonReceiveMessageHandler; | ||||
| import org.asamk.signal.JsonWriter; | ||||
| import org.asamk.signal.OutputType; | ||||
| import org.asamk.signal.OutputWriter; | ||||
| import org.asamk.signal.commands.exceptions.CommandException; | ||||
| import org.asamk.signal.commands.exceptions.IOErrorException; | ||||
| import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException; | ||||
| import org.asamk.signal.commands.exceptions.UserErrorException; | ||||
| import org.asamk.signal.jsonrpc.JsonRpcException; | ||||
| import org.asamk.signal.jsonrpc.JsonRpcReader; | ||||
| import org.asamk.signal.jsonrpc.JsonRpcRequest; | ||||
| import org.asamk.signal.jsonrpc.JsonRpcResponse; | ||||
| import org.asamk.signal.jsonrpc.JsonRpcSender; | ||||
| import org.asamk.signal.manager.Manager; | ||||
| import org.asamk.signal.util.Util; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| 
 | ||||
| import java.io.BufferedReader; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStreamReader; | ||||
| import java.util.Set; | ||||
| import java.util.concurrent.TimeUnit; | ||||
| 
 | ||||
| public class JsonRpcDispatcherCommand implements LocalCommand { | ||||
| 
 | ||||
|     private final static Logger logger = LoggerFactory.getLogger(JsonRpcDispatcherCommand.class); | ||||
| 
 | ||||
|     private static final int USER_ERROR = -1; | ||||
|     private static final int IO_ERROR = -3; | ||||
|     private static final int UNTRUSTED_KEY_ERROR = -4; | ||||
| 
 | ||||
|     private final OutputWriter outputWriter; | ||||
| 
 | ||||
|     public static void attachToSubparser(final Subparser subparser) { | ||||
|         subparser.help("Take commands from standard input as line-delimited JSON RPC while receiving messages."); | ||||
|         subparser.addArgument("--ignore-attachments") | ||||
|                 .help("Don’t download attachments of received messages.") | ||||
|                 .action(Arguments.storeTrue()); | ||||
|     } | ||||
| 
 | ||||
|     public JsonRpcDispatcherCommand(final OutputWriter outputWriter) { | ||||
|         this.outputWriter = outputWriter; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Set<OutputType> getSupportedOutputTypes() { | ||||
|         return Set.of(OutputType.JSON); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleCommand(final Namespace ns, final Manager m) throws CommandException { | ||||
|         final boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); | ||||
| 
 | ||||
|         final var objectMapper = Util.createJsonObjectMapper(); | ||||
|         final var jsonRpcSender = new JsonRpcSender((JsonWriter) outputWriter); | ||||
| 
 | ||||
|         final var receiveThread = receiveMessages(s -> jsonRpcSender.sendRequest(JsonRpcRequest.forNotification( | ||||
|                 "receive", | ||||
|                 objectMapper.valueToTree(s), | ||||
|                 null)), m, ignoreAttachments); | ||||
| 
 | ||||
|         final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); | ||||
| 
 | ||||
|         final var jsonRpcReader = new JsonRpcReader(jsonRpcSender, () -> { | ||||
|             try { | ||||
|                 return reader.readLine(); | ||||
|             } catch (IOException e) { | ||||
|                 throw new AssertionError(e); | ||||
|             } | ||||
|         }); | ||||
|         jsonRpcReader.readRequests((method, params) -> handleRequest(m, objectMapper, method, params), | ||||
|                 response -> logger.debug("Received unexpected response for id {}", response.getId())); | ||||
| 
 | ||||
|         receiveThread.interrupt(); | ||||
|         try { | ||||
|             receiveThread.join(); | ||||
|         } catch (InterruptedException ignored) { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private JsonNode handleRequest( | ||||
|             final Manager m, final ObjectMapper objectMapper, final String method, ContainerNode<?> params | ||||
|     ) throws JsonRpcException { | ||||
|         final Object[] result = {null}; | ||||
|         final JsonWriter commandOutputWriter = s -> { | ||||
|             if (result[0] != null) { | ||||
|                 throw new AssertionError("Command may only write one json result"); | ||||
|             } | ||||
| 
 | ||||
|             result[0] = s; | ||||
|         }; | ||||
| 
 | ||||
|         var command = Commands.getCommand(method, commandOutputWriter); | ||||
|         if (!(command instanceof JsonRpcCommand)) { | ||||
|             throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.METHOD_NOT_FOUND, | ||||
|                     "Method not implemented", | ||||
|                     null)); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             parseParamsAndRunCommand(m, objectMapper, params, (JsonRpcCommand<?>) command); | ||||
|         } catch (JsonMappingException e) { | ||||
|             throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST, | ||||
|                     e.getMessage(), | ||||
|                     null)); | ||||
|         } catch (UserErrorException e) { | ||||
|             throw new JsonRpcException(new JsonRpcResponse.Error(USER_ERROR, e.getMessage(), null)); | ||||
|         } catch (IOErrorException e) { | ||||
|             throw new JsonRpcException(new JsonRpcResponse.Error(IO_ERROR, e.getMessage(), null)); | ||||
|         } catch (UntrustedKeyErrorException e) { | ||||
|             throw new JsonRpcException(new JsonRpcResponse.Error(UNTRUSTED_KEY_ERROR, e.getMessage(), null)); | ||||
|         } catch (Throwable e) { | ||||
|             logger.error("Command execution failed", e); | ||||
|             throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INTERNAL_ERROR, | ||||
|                     e.getMessage(), | ||||
|                     null)); | ||||
|         } | ||||
| 
 | ||||
|         Object output = result[0] == null ? new Object() : result[0]; | ||||
|         return objectMapper.valueToTree(output); | ||||
|     } | ||||
| 
 | ||||
|     private <T> void parseParamsAndRunCommand( | ||||
|             final Manager m, final ObjectMapper objectMapper, final TreeNode params, final JsonRpcCommand<T> command | ||||
|     ) throws CommandException, JsonMappingException { | ||||
|         T requestParams = null; | ||||
|         final var requestType = command.getRequestType(); | ||||
|         if (params != null && requestType != null) { | ||||
|             try { | ||||
|                 requestParams = objectMapper.readValue(objectMapper.treeAsTokens(params), requestType); | ||||
|             } catch (JsonMappingException e) { | ||||
|                 throw e; | ||||
|             } catch (IOException e) { | ||||
|                 throw new AssertionError(e); | ||||
|             } | ||||
|         } | ||||
|         command.handleCommand(requestParams, m); | ||||
|     } | ||||
| 
 | ||||
|     private Thread receiveMessages( | ||||
|             JsonWriter jsonWriter, Manager m, boolean ignoreAttachments | ||||
|     ) { | ||||
|         final var thread = new Thread(() -> { | ||||
|             while (!Thread.interrupted()) { | ||||
|                 try { | ||||
|                     final var receiveMessageHandler = new JsonReceiveMessageHandler(m, jsonWriter); | ||||
|                     m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, receiveMessageHandler); | ||||
|                     break; | ||||
|                 } catch (IOException e) { | ||||
|                     logger.warn("Receiving messages failed, retrying", e); | ||||
|                 } catch (InterruptedException e) { | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         thread.start(); | ||||
| 
 | ||||
|         return thread; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,72 @@ | ||||
| package org.asamk.signal.commands; | ||||
| 
 | ||||
| import com.fasterxml.jackson.core.type.TypeReference; | ||||
| 
 | ||||
| import net.sourceforge.argparse4j.inf.Namespace; | ||||
| 
 | ||||
| import org.asamk.signal.OutputType; | ||||
| import org.asamk.signal.commands.exceptions.CommandException; | ||||
| import org.asamk.signal.manager.Manager; | ||||
| import org.asamk.signal.util.Util; | ||||
| 
 | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| public interface JsonRpcLocalCommand extends JsonRpcCommand<Map<String, Object>> { | ||||
| 
 | ||||
|     void handleCommand(Namespace ns, Manager m) throws CommandException; | ||||
| 
 | ||||
|     default TypeReference<Map<String, Object>> getRequestType() { | ||||
|         return new TypeReference<>() { | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     default void handleCommand(Map<String, Object> request, Manager m) throws CommandException { | ||||
|         Namespace commandNamespace = new JsonRpcNamespace(request == null ? Map.of() : request); | ||||
|         handleCommand(commandNamespace, m); | ||||
|     } | ||||
| 
 | ||||
|     default Set<OutputType> getSupportedOutputTypes() { | ||||
|         return Set.of(OutputType.PLAIN_TEXT, OutputType.JSON); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Namepace implementation, that defaults booleans to false and converts camel case keys to dashed strings | ||||
|      */ | ||||
|     final class JsonRpcNamespace extends Namespace { | ||||
| 
 | ||||
|         public JsonRpcNamespace(final Map<String, Object> attrs) { | ||||
|             super(attrs); | ||||
|         } | ||||
| 
 | ||||
|         public <T> T get(String dest) { | ||||
|             final T value = super.get(dest); | ||||
|             if (value != null) { | ||||
|                 return value; | ||||
|             } | ||||
| 
 | ||||
|             final var camelCaseString = Util.dashSeparatedToCamelCaseString(dest); | ||||
|             return super.get(camelCaseString); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public <E> List<E> getList(final String dest) { | ||||
|             final List<E> value = super.getList(dest); | ||||
|             if (value != null) { | ||||
|                 return value; | ||||
|             } | ||||
| 
 | ||||
|             return super.getList(dest + "s"); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public Boolean getBoolean(String dest) { | ||||
|             Boolean maybeGotten = this.get(dest); | ||||
|             if (maybeGotten == null) { | ||||
|                 maybeGotten = false; | ||||
|             } | ||||
|             return maybeGotten; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -5,7 +5,6 @@ import net.sourceforge.argparse4j.inf.Namespace; | ||||
| import net.sourceforge.argparse4j.inf.Subparser; | ||||
| 
 | ||||
| import org.asamk.signal.JsonWriter; | ||||
| import org.asamk.signal.OutputType; | ||||
| import org.asamk.signal.OutputWriter; | ||||
| import org.asamk.signal.PlainTextWriter; | ||||
| import org.asamk.signal.commands.exceptions.CommandException; | ||||
| @ -19,7 +18,7 @@ import org.slf4j.LoggerFactory; | ||||
| import java.util.Set; | ||||
| import java.util.stream.Collectors; | ||||
| 
 | ||||
| public class ListGroupsCommand implements LocalCommand { | ||||
| public class ListGroupsCommand implements JsonRpcLocalCommand { | ||||
| 
 | ||||
|     private final static Logger logger = LoggerFactory.getLogger(ListGroupsCommand.class); | ||||
| 
 | ||||
| @ -70,11 +69,6 @@ public class ListGroupsCommand implements LocalCommand { | ||||
|         this.outputWriter = outputWriter; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Set<OutputType> getSupportedOutputTypes() { | ||||
|         return Set.of(OutputType.PLAIN_TEXT, OutputType.JSON); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleCommand(final Namespace ns, final Manager m) throws CommandException { | ||||
|         final var groups = m.getGroups(); | ||||
|  | ||||
| @ -155,6 +155,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { | ||||
|                     handler); | ||||
|         } catch (IOException e) { | ||||
|             throw new IOErrorException("Error while receiving messages: " + e.getMessage()); | ||||
|         } catch (InterruptedException ignored) { | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -5,12 +5,15 @@ import net.sourceforge.argparse4j.inf.Namespace; | ||||
| import net.sourceforge.argparse4j.inf.Subparser; | ||||
| 
 | ||||
| import org.asamk.Signal; | ||||
| import org.asamk.signal.JsonWriter; | ||||
| import org.asamk.signal.OutputWriter; | ||||
| import org.asamk.signal.PlainTextWriterImpl; | ||||
| import org.asamk.signal.PlainTextWriter; | ||||
| import org.asamk.signal.commands.exceptions.CommandException; | ||||
| import org.asamk.signal.commands.exceptions.UnexpectedErrorException; | ||||
| import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException; | ||||
| import org.asamk.signal.commands.exceptions.UserErrorException; | ||||
| import org.asamk.signal.dbus.DbusSignalImpl; | ||||
| import org.asamk.signal.manager.Manager; | ||||
| import org.asamk.signal.manager.groups.GroupIdFormatException; | ||||
| import org.asamk.signal.util.IOUtils; | ||||
| import org.asamk.signal.util.Util; | ||||
| @ -22,8 +25,9 @@ import org.slf4j.LoggerFactory; | ||||
| import java.io.IOException; | ||||
| import java.nio.charset.Charset; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| public class SendCommand implements DbusCommand { | ||||
| public class SendCommand implements DbusCommand, JsonRpcLocalCommand { | ||||
| 
 | ||||
|     private final static Logger logger = LoggerFactory.getLogger(SendCommand.class); | ||||
|     private final OutputWriter outputWriter; | ||||
| @ -92,8 +96,6 @@ public class SendCommand implements DbusCommand { | ||||
|             attachments = List.of(); | ||||
|         } | ||||
| 
 | ||||
|         final var writer = (PlainTextWriterImpl) outputWriter; | ||||
| 
 | ||||
|         if (groupIdString != null) { | ||||
|             byte[] groupId; | ||||
|             try { | ||||
| @ -104,7 +106,7 @@ public class SendCommand implements DbusCommand { | ||||
| 
 | ||||
|             try { | ||||
|                 var timestamp = signal.sendGroupMessage(messageText, attachments, groupId); | ||||
|                 writer.println("{}", timestamp); | ||||
|                 outputResult(timestamp); | ||||
|                 return; | ||||
|             } catch (DBusExecutionException e) { | ||||
|                 throw new UnexpectedErrorException("Failed to send group message: " + e.getMessage()); | ||||
| @ -114,7 +116,7 @@ public class SendCommand implements DbusCommand { | ||||
|         if (isNoteToSelf) { | ||||
|             try { | ||||
|                 var timestamp = signal.sendNoteToSelfMessage(messageText, attachments); | ||||
|                 writer.println("{}", timestamp); | ||||
|                 outputResult(timestamp); | ||||
|                 return; | ||||
|             } catch (Signal.Error.UntrustedIdentity e) { | ||||
|                 throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage()); | ||||
| @ -125,7 +127,7 @@ public class SendCommand implements DbusCommand { | ||||
| 
 | ||||
|         try { | ||||
|             var timestamp = signal.sendMessage(messageText, attachments, recipients); | ||||
|             writer.println("{}", timestamp); | ||||
|             outputResult(timestamp); | ||||
|         } catch (UnknownObject e) { | ||||
|             throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage()); | ||||
|         } catch (Signal.Error.UntrustedIdentity e) { | ||||
| @ -134,4 +136,19 @@ public class SendCommand implements DbusCommand { | ||||
|             throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void outputResult(final long timestamp) { | ||||
|         if (outputWriter instanceof PlainTextWriter) { | ||||
|             final var writer = (PlainTextWriter) outputWriter; | ||||
|             writer.println("{}", timestamp); | ||||
|         } else { | ||||
|             final var writer = (JsonWriter) outputWriter; | ||||
|             writer.write(Map.of("timestamp", timestamp)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleCommand(final Namespace ns, final Manager m) throws CommandException { | ||||
|         handleCommand(ns, new DbusSignalImpl(m, null)); | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										24
									
								
								src/main/java/org/asamk/signal/commands/VersionCommand.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/main/java/org/asamk/signal/commands/VersionCommand.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| package org.asamk.signal.commands; | ||||
| 
 | ||||
| import org.asamk.signal.BaseConfig; | ||||
| import org.asamk.signal.JsonWriter; | ||||
| import org.asamk.signal.OutputWriter; | ||||
| import org.asamk.signal.commands.exceptions.CommandException; | ||||
| import org.asamk.signal.manager.Manager; | ||||
| 
 | ||||
| import java.util.Map; | ||||
| 
 | ||||
| public class VersionCommand implements JsonRpcCommand<Void> { | ||||
| 
 | ||||
|     private final OutputWriter outputWriter; | ||||
| 
 | ||||
|     public VersionCommand(final OutputWriter outputWriter) { | ||||
|         this.outputWriter = outputWriter; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleCommand(final Void request, final Manager m) throws CommandException { | ||||
|         final var jsonWriter = (JsonWriter) outputWriter; | ||||
|         jsonWriter.write(Map.of("version", BaseConfig.PROJECT_VERSION)); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,18 @@ | ||||
| package org.asamk.signal.jsonrpc; | ||||
| 
 | ||||
| import com.fasterxml.jackson.databind.JsonNode; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| public class JsonRpcBulkMessage extends JsonRpcMessage { | ||||
| 
 | ||||
|     List<JsonNode> messages; | ||||
| 
 | ||||
|     public JsonRpcBulkMessage(final List<JsonNode> messages) { | ||||
|         this.messages = messages; | ||||
|     } | ||||
| 
 | ||||
|     public List<JsonNode> getMessages() { | ||||
|         return messages; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										14
									
								
								src/main/java/org/asamk/signal/jsonrpc/JsonRpcException.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/main/java/org/asamk/signal/jsonrpc/JsonRpcException.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| package org.asamk.signal.jsonrpc; | ||||
| 
 | ||||
| public class JsonRpcException extends Exception { | ||||
| 
 | ||||
|     private final JsonRpcResponse.Error error; | ||||
| 
 | ||||
|     public JsonRpcException(final JsonRpcResponse.Error error) { | ||||
|         this.error = error; | ||||
|     } | ||||
| 
 | ||||
|     public JsonRpcResponse.Error getError() { | ||||
|         return error; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,9 @@ | ||||
| package org.asamk.signal.jsonrpc; | ||||
| 
 | ||||
| /** | ||||
|  * Represents a JSON-RPC (bulk) request or (bulk) response. | ||||
|  * https://www.jsonrpc.org/specification | ||||
|  */ | ||||
| public abstract class JsonRpcMessage { | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										216
									
								
								src/main/java/org/asamk/signal/jsonrpc/JsonRpcReader.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								src/main/java/org/asamk/signal/jsonrpc/JsonRpcReader.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,216 @@ | ||||
| package org.asamk.signal.jsonrpc; | ||||
| 
 | ||||
| import com.fasterxml.jackson.core.JsonParseException; | ||||
| import com.fasterxml.jackson.databind.JsonMappingException; | ||||
| import com.fasterxml.jackson.databind.JsonNode; | ||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||
| import com.fasterxml.jackson.databind.node.ContainerNode; | ||||
| import com.fasterxml.jackson.databind.node.ValueNode; | ||||
| 
 | ||||
| import org.asamk.signal.util.Util; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.util.Objects; | ||||
| import java.util.function.Consumer; | ||||
| import java.util.function.Supplier; | ||||
| import java.util.stream.Collectors; | ||||
| import java.util.stream.StreamSupport; | ||||
| 
 | ||||
| public class JsonRpcReader { | ||||
| 
 | ||||
|     private final static Logger logger = LoggerFactory.getLogger(JsonRpcReader.class); | ||||
| 
 | ||||
|     private final JsonRpcSender jsonRpcSender; | ||||
|     private final ObjectMapper objectMapper; | ||||
|     private final Supplier<String> lineSupplier; | ||||
| 
 | ||||
|     public JsonRpcReader( | ||||
|             final JsonRpcSender jsonRpcSender, final Supplier<String> lineSupplier | ||||
|     ) { | ||||
|         this.jsonRpcSender = jsonRpcSender; | ||||
|         this.lineSupplier = lineSupplier; | ||||
|         this.objectMapper = Util.createJsonObjectMapper(); | ||||
|     } | ||||
| 
 | ||||
|     public void readRequests( | ||||
|             final RequestHandler requestHandler, final Consumer<JsonRpcResponse> responseHandler | ||||
|     ) { | ||||
|         while (!Thread.interrupted()) { | ||||
|             JsonRpcMessage message = readMessage(); | ||||
|             if (message == null) break; | ||||
| 
 | ||||
|             if (message instanceof JsonRpcRequest) { | ||||
|                 final var response = handleRequest(requestHandler, (JsonRpcRequest) message); | ||||
|                 if (response != null) { | ||||
|                     jsonRpcSender.sendResponse(response); | ||||
|                 } | ||||
|             } else if (message instanceof JsonRpcResponse) { | ||||
|                 responseHandler.accept((JsonRpcResponse) message); | ||||
|             } else { | ||||
|                 final var responseList = ((JsonRpcBulkMessage) message).getMessages().stream().map(jsonNode -> { | ||||
|                     final JsonRpcRequest request; | ||||
|                     try { | ||||
|                         request = parseJsonRpcRequest(jsonNode); | ||||
|                     } catch (JsonRpcException e) { | ||||
|                         return JsonRpcResponse.forError(e.getError(), getId(jsonNode)); | ||||
|                     } | ||||
| 
 | ||||
|                     return handleRequest(requestHandler, request); | ||||
|                 }).filter(Objects::nonNull).collect(Collectors.toList()); | ||||
| 
 | ||||
|                 jsonRpcSender.sendBulkResponses(responseList); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private JsonRpcResponse handleRequest(final RequestHandler requestHandler, final JsonRpcRequest request) { | ||||
|         try { | ||||
|             final var result = requestHandler.apply(request.getMethod(), request.getParams()); | ||||
|             if (request.getId() != null) { | ||||
|                 return JsonRpcResponse.forSuccess(result, request.getId()); | ||||
|             } | ||||
|         } catch (JsonRpcException e) { | ||||
|             if (request.getId() != null) { | ||||
|                 return JsonRpcResponse.forError(e.getError(), request.getId()); | ||||
|             } | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     private JsonRpcMessage readMessage() { | ||||
|         while (!Thread.interrupted()) { | ||||
|             String input = lineSupplier.get(); | ||||
| 
 | ||||
|             if (input == null) { | ||||
|                 // Reached end of input stream | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             JsonRpcMessage message = parseJsonRpcMessage(input); | ||||
|             if (message == null) continue; | ||||
| 
 | ||||
|             return message; | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     private JsonRpcMessage parseJsonRpcMessage(final String input) { | ||||
|         final JsonNode jsonNode; | ||||
|         try { | ||||
|             jsonNode = objectMapper.readTree(input); | ||||
|         } catch (JsonParseException e) { | ||||
|             jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.PARSE_ERROR, | ||||
|                     e.getMessage(), | ||||
|                     null), null)); | ||||
|             return null; | ||||
|         } catch (IOException e) { | ||||
|             throw new AssertionError(e); | ||||
|         } | ||||
| 
 | ||||
|         if (jsonNode == null) { | ||||
|             jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST, | ||||
|                     "invalid request", | ||||
|                     null), null)); | ||||
|             return null; | ||||
|         } else if (jsonNode.isArray()) { | ||||
|             if (jsonNode.size() == 0) { | ||||
|                 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST, | ||||
|                         "invalid request", | ||||
|                         null), null)); | ||||
|                 return null; | ||||
|             } | ||||
|             return new JsonRpcBulkMessage(StreamSupport.stream(jsonNode.spliterator(), false) | ||||
|                     .collect(Collectors.toList())); | ||||
|         } else if (jsonNode.isObject()) { | ||||
|             if (jsonNode.has("result") || jsonNode.has("error")) { | ||||
|                 return parseJsonRpcResponse(jsonNode); | ||||
|             } else { | ||||
|                 try { | ||||
|                     return parseJsonRpcRequest(jsonNode); | ||||
|                 } catch (JsonRpcException e) { | ||||
|                     jsonRpcSender.sendResponse(JsonRpcResponse.forError(e.getError(), getId(jsonNode))); | ||||
|                     return null; | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST, | ||||
|                     "unexpected type: " + jsonNode.getNodeType().name(), | ||||
|                     null), null)); | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private ValueNode getId(JsonNode jsonNode) { | ||||
|         final var id = jsonNode.get("id"); | ||||
|         return id instanceof ValueNode ? (ValueNode) id : null; | ||||
|     } | ||||
| 
 | ||||
|     private JsonRpcRequest parseJsonRpcRequest(final JsonNode input) throws JsonRpcException { | ||||
|         JsonRpcRequest request; | ||||
|         try { | ||||
|             request = objectMapper.treeToValue(input, JsonRpcRequest.class); | ||||
|         } catch (JsonMappingException e) { | ||||
|             throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST, | ||||
|                     e.getMessage(), | ||||
|                     null)); | ||||
|         } catch (IOException e) { | ||||
|             throw new AssertionError(e); | ||||
|         } | ||||
| 
 | ||||
|         if (!"2.0".equals(request.getJsonrpc())) { | ||||
|             throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST, | ||||
|                     "only jsonrpc version 2.0 is supported", | ||||
|                     null)); | ||||
|         } | ||||
| 
 | ||||
|         if (request.getMethod() == null) { | ||||
|             throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST, | ||||
|                     "method field must be set", | ||||
|                     null)); | ||||
|         } | ||||
| 
 | ||||
|         return request; | ||||
|     } | ||||
| 
 | ||||
|     private JsonRpcResponse parseJsonRpcResponse(final JsonNode input) { | ||||
|         JsonRpcResponse response; | ||||
|         try { | ||||
|             response = objectMapper.treeToValue(input, JsonRpcResponse.class); | ||||
|         } catch (JsonParseException | JsonMappingException e) { | ||||
|             logger.debug("Received invalid jsonrpc response {}", e.getMessage()); | ||||
|             return null; | ||||
|         } catch (IOException e) { | ||||
|             throw new AssertionError(e); | ||||
|         } | ||||
| 
 | ||||
|         if (!"2.0".equals(response.getJsonrpc())) { | ||||
|             logger.debug("Received invalid jsonrpc response with invalid version {}", response.getJsonrpc()); | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         if (response.getResult() != null && response.getError() != null) { | ||||
|             logger.debug("Received invalid jsonrpc response with both result and error"); | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         if (response.getResult() == null && response.getError() == null) { | ||||
|             logger.debug("Received invalid jsonrpc response without result and error"); | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         if (response.getId() == null || response.getId().isNull()) { | ||||
|             logger.debug("Received invalid jsonrpc response without id"); | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         return response; | ||||
|     } | ||||
| 
 | ||||
|     public interface RequestHandler { | ||||
| 
 | ||||
|         JsonNode apply(String method, ContainerNode<?> params) throws JsonRpcException; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										73
									
								
								src/main/java/org/asamk/signal/jsonrpc/JsonRpcRequest.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/main/java/org/asamk/signal/jsonrpc/JsonRpcRequest.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | ||||
| package org.asamk.signal.jsonrpc; | ||||
| 
 | ||||
| import com.fasterxml.jackson.annotation.JsonInclude; | ||||
| import com.fasterxml.jackson.databind.node.ContainerNode; | ||||
| import com.fasterxml.jackson.databind.node.ValueNode; | ||||
| 
 | ||||
| /** | ||||
|  * Represents a JSON-RPC request. | ||||
|  * https://www.jsonrpc.org/specification#request_object | ||||
|  */ | ||||
| public class JsonRpcRequest extends JsonRpcMessage { | ||||
| 
 | ||||
|     /** | ||||
|      * A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0". | ||||
|      */ | ||||
|     String jsonrpc; | ||||
| 
 | ||||
|     /** | ||||
|      * A String containing the name of the method to be invoked. | ||||
|      * Method names that begin with the word rpc followed by a period character (U+002E or ASCII 46) | ||||
|      * are reserved for rpc-internal methods and extensions and MUST NOT be used for anything else. | ||||
|      */ | ||||
|     String method; | ||||
| 
 | ||||
|     /** | ||||
|      * A Structured value that holds the parameter values to be used during the invocation of the method. | ||||
|      * This member MAY be omitted. | ||||
|      */ | ||||
|     @JsonInclude(JsonInclude.Include.NON_NULL) | ||||
|     ContainerNode<?> params; | ||||
| 
 | ||||
|     /** | ||||
|      * An identifier established by the Client that MUST contain a String, Number, or NULL value if included. | ||||
|      * If it is not included it is assumed to be a notification. | ||||
|      * The value SHOULD normally not be Null and Numbers SHOULD NOT contain fractional parts | ||||
|      */ | ||||
|     @JsonInclude(JsonInclude.Include.NON_NULL) | ||||
|     ValueNode id; | ||||
| 
 | ||||
|     public static JsonRpcRequest forNotification( | ||||
|             final String method, final ContainerNode<?> params, final ValueNode id | ||||
|     ) { | ||||
|         return new JsonRpcRequest("2.0", method, params, id); | ||||
|     } | ||||
| 
 | ||||
|     private JsonRpcRequest() { | ||||
|     } | ||||
| 
 | ||||
|     private JsonRpcRequest( | ||||
|             final String jsonrpc, final String method, final ContainerNode<?> params, final ValueNode id | ||||
|     ) { | ||||
|         this.jsonrpc = jsonrpc; | ||||
|         this.method = method; | ||||
|         this.params = params; | ||||
|         this.id = id; | ||||
|     } | ||||
| 
 | ||||
|     public String getJsonrpc() { | ||||
|         return jsonrpc; | ||||
|     } | ||||
| 
 | ||||
|     public String getMethod() { | ||||
|         return method; | ||||
|     } | ||||
| 
 | ||||
|     public ContainerNode<?> getParams() { | ||||
|         return params; | ||||
|     } | ||||
| 
 | ||||
|     public ValueNode getId() { | ||||
|         return id; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										120
									
								
								src/main/java/org/asamk/signal/jsonrpc/JsonRpcResponse.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/main/java/org/asamk/signal/jsonrpc/JsonRpcResponse.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,120 @@ | ||||
| package org.asamk.signal.jsonrpc; | ||||
| 
 | ||||
| import com.fasterxml.jackson.annotation.JsonInclude; | ||||
| import com.fasterxml.jackson.databind.JsonNode; | ||||
| import com.fasterxml.jackson.databind.node.ValueNode; | ||||
| 
 | ||||
| /** | ||||
|  * Represents a JSON-RPC response. | ||||
|  * https://www.jsonrpc.org/specification#response_object | ||||
|  */ | ||||
| public class JsonRpcResponse extends JsonRpcMessage { | ||||
| 
 | ||||
|     /** | ||||
|      * A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0". | ||||
|      */ | ||||
|     String jsonrpc; | ||||
| 
 | ||||
|     /** | ||||
|      * This member is REQUIRED on success. | ||||
|      * This member MUST NOT exist if there was an error invoking the method. | ||||
|      * The value of this member is determined by the method invoked on the Server. | ||||
|      */ | ||||
|     @JsonInclude(JsonInclude.Include.NON_NULL) | ||||
|     JsonNode result; | ||||
| 
 | ||||
|     /** | ||||
|      * This member is REQUIRED on error. | ||||
|      * This member MUST NOT exist if there was no error triggered during invocation. | ||||
|      * The value for this member MUST be an Object as defined in section 5.1. | ||||
|      */ | ||||
|     @JsonInclude(JsonInclude.Include.NON_NULL) | ||||
|     Error error; | ||||
| 
 | ||||
|     /** | ||||
|      * This member is REQUIRED. | ||||
|      * It MUST be the same as the value of the id member in the Request Object. | ||||
|      * If there was an error in detecting the id in the Request object (e.g. Parse error/Invalid Request), it MUST be Null. | ||||
|      */ | ||||
|     ValueNode id; | ||||
| 
 | ||||
|     public static JsonRpcResponse forSuccess(JsonNode result, ValueNode id) { | ||||
|         return new JsonRpcResponse("2.0", result, null, id); | ||||
|     } | ||||
| 
 | ||||
|     public static JsonRpcResponse forError(Error error, ValueNode id) { | ||||
|         return new JsonRpcResponse("2.0", null, error, id); | ||||
|     } | ||||
| 
 | ||||
|     private JsonRpcResponse() { | ||||
|     } | ||||
| 
 | ||||
|     private JsonRpcResponse(final String jsonrpc, final JsonNode result, final Error error, final ValueNode id) { | ||||
|         this.jsonrpc = jsonrpc; | ||||
|         this.result = result; | ||||
|         this.error = error; | ||||
|         this.id = id; | ||||
|     } | ||||
| 
 | ||||
|     public String getJsonrpc() { | ||||
|         return jsonrpc; | ||||
|     } | ||||
| 
 | ||||
|     public JsonNode getResult() { | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     public Error getError() { | ||||
|         return error; | ||||
|     } | ||||
| 
 | ||||
|     public ValueNode getId() { | ||||
|         return id; | ||||
|     } | ||||
| 
 | ||||
|     public static class Error { | ||||
| 
 | ||||
|         public static final int PARSE_ERROR = -32700; | ||||
|         public static final int INVALID_REQUEST = -32600; | ||||
|         public static final int METHOD_NOT_FOUND = -32601; | ||||
|         public static final int INVALID_PARAMS = -32602; | ||||
|         public static final int INTERNAL_ERROR = -32603; | ||||
| 
 | ||||
|         /** | ||||
|          * A Number that indicates the error type that occurred. | ||||
|          * This MUST be an integer. | ||||
|          */ | ||||
|         int code; | ||||
| 
 | ||||
|         /** | ||||
|          * A String providing a short description of the error. | ||||
|          * The message SHOULD be limited to a concise single sentence. | ||||
|          */ | ||||
|         String message; | ||||
| 
 | ||||
|         /** | ||||
|          * A Primitive or Structured value that contains additional information about the error. | ||||
|          * This may be omitted. | ||||
|          * The value of this member is defined by the Server (e.g. detailed error information, nested errors etc.). | ||||
|          */ | ||||
|         JsonNode data; | ||||
| 
 | ||||
|         public Error(final int code, final String message, final JsonNode data) { | ||||
|             this.code = code; | ||||
|             this.message = message; | ||||
|             this.data = data; | ||||
|         } | ||||
| 
 | ||||
|         public int getCode() { | ||||
|             return code; | ||||
|         } | ||||
| 
 | ||||
|         public String getMessage() { | ||||
|             return message; | ||||
|         } | ||||
| 
 | ||||
|         public JsonNode getData() { | ||||
|             return data; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										30
									
								
								src/main/java/org/asamk/signal/jsonrpc/JsonRpcSender.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/main/java/org/asamk/signal/jsonrpc/JsonRpcSender.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| package org.asamk.signal.jsonrpc; | ||||
| 
 | ||||
| import org.asamk.signal.JsonWriter; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| public class JsonRpcSender { | ||||
| 
 | ||||
|     private final JsonWriter jsonWriter; | ||||
| 
 | ||||
|     public JsonRpcSender(final JsonWriter jsonWriter) { | ||||
|         this.jsonWriter = jsonWriter; | ||||
|     } | ||||
| 
 | ||||
|     public void sendRequest(JsonRpcRequest request) { | ||||
|         jsonWriter.write(request); | ||||
|     } | ||||
| 
 | ||||
|     public void sendBulkRequests(List<JsonRpcRequest> requests) { | ||||
|         jsonWriter.write(requests); | ||||
|     } | ||||
| 
 | ||||
|     public void sendResponse(JsonRpcResponse response) { | ||||
|         jsonWriter.write(response); | ||||
|     } | ||||
| 
 | ||||
|     public void sendBulkResponses(List<JsonRpcResponse> responses) { | ||||
|         jsonWriter.write(responses); | ||||
|     } | ||||
| } | ||||
| @ -1,10 +1,20 @@ | ||||
| package org.asamk.signal.util; | ||||
| 
 | ||||
| import com.fasterxml.jackson.annotation.JsonAutoDetect; | ||||
| import com.fasterxml.jackson.annotation.PropertyAccessor; | ||||
| import com.fasterxml.jackson.core.JsonGenerator; | ||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||
| 
 | ||||
| import org.asamk.signal.manager.groups.GroupId; | ||||
| import org.asamk.signal.manager.groups.GroupIdFormatException; | ||||
| import org.whispersystems.libsignal.util.guava.Optional; | ||||
| import org.whispersystems.signalservice.api.push.SignalServiceAddress; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.stream.Collectors; | ||||
| 
 | ||||
| public class Util { | ||||
| 
 | ||||
|     private Util() { | ||||
| @ -18,6 +28,22 @@ public class Util { | ||||
|         return string; | ||||
|     } | ||||
| 
 | ||||
|     public static String dashSeparatedToCamelCaseString(String s) { | ||||
|         var parts = s.split("-"); | ||||
|         return toCamelCaseString(Arrays.asList(parts)); | ||||
|     } | ||||
| 
 | ||||
|     private static String toCamelCaseString(List<String> strings) { | ||||
|         if (strings.size() == 0) { | ||||
|             return ""; | ||||
|         } | ||||
|         return strings.get(0) + strings.stream() | ||||
|                 .skip(1) | ||||
|                 .filter(s -> s.length() > 0) | ||||
|                 .map(s -> Character.toUpperCase(s.charAt(0)) + s.substring(1).toLowerCase(Locale.ROOT)) | ||||
|                 .collect(Collectors.joining()); | ||||
|     } | ||||
| 
 | ||||
|     public static String formatSafetyNumber(String digits) { | ||||
|         final var partCount = 12; | ||||
|         var partSize = digits.length() / partCount; | ||||
| @ -35,4 +61,11 @@ public class Util { | ||||
|     public static String getLegacyIdentifier(final SignalServiceAddress address) { | ||||
|         return address.getNumber().or(() -> address.getUuid().get().toString()); | ||||
|     } | ||||
| 
 | ||||
|     public static ObjectMapper createJsonObjectMapper() { | ||||
|         var objectMapper = new ObjectMapper(); | ||||
|         objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY); | ||||
|         objectMapper.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); | ||||
|         return objectMapper; | ||||
|     } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 AsamK
						AsamK