From 44d54b32157742fda0c09bc56937f456fa1329fe Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 16:35:22 +0200 Subject: [PATCH] Add support for a global configuration file --- man/signal-cli-dbus.5.adoc | 2 +- man/signal-cli-jsonrpc.5.adoc | 4 +- man/signal-cli.1.adoc | 23 +++++- src/main/java/org/asamk/signal/App.java | 55 ++++++++----- .../java/org/asamk/signal/ConfigLoader.java | 81 +++++++++++++++++++ .../java/org/asamk/signal/GlobalConfig.java | 36 +++++++++ src/main/java/org/asamk/signal/Main.java | 64 ++++++++++----- .../java/org/asamk/signal/OutputType.java | 15 +++- .../asamk/signal/ServiceEnvironmentCli.java | 15 +++- .../org/asamk/signal/TrustNewIdentityCli.java | 16 +++- .../signal-cli/reachability-metadata.json | 55 +++++++++++++ 11 files changed, 315 insertions(+), 51 deletions(-) create mode 100644 src/main/java/org/asamk/signal/ConfigLoader.java create mode 100644 src/main/java/org/asamk/signal/GlobalConfig.java diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 732efe43..9246e9ed 100644 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -13,7 +13,7 @@ signal-cli-dbus - A commandline and dbus interface for the Signal messenger == Synopsis -*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] [-o {plain-text,json}] daemon [--dbus] [--dbus-system] +*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] [-o {plain-text,json}] daemon [--dbus] [--dbus-system] *dbus-send* [--system | --session] [--print-reply] --type=method_call --dest="org.asamk.Signal" /org/asamk/Signal[/_] org.asamk.Signal. [string:] [array::] diff --git a/man/signal-cli-jsonrpc.5.adoc b/man/signal-cli-jsonrpc.5.adoc index 1c035af8..b9abb8ce 100644 --- a/man/signal-cli-jsonrpc.5.adoc +++ b/man/signal-cli-jsonrpc.5.adoc @@ -13,9 +13,9 @@ signal-cli-jsonrpc - A commandline and dbus interface for the Signal messenger == Synopsis -*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] daemon [--socket[=SOCKET_PATH]] [--tcp[=HOST:PORT]] [--http[=HOST:PORT]] +*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] daemon [--socket[=SOCKET_PATH]] [--tcp[=HOST:PORT]] [--http[=HOST:PORT]] -*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] jsonRpc +*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] jsonRpc == Description diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index e349a2ad..f6ed732d 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -13,7 +13,7 @@ signal-cli - A commandline interface for the Signal messenger == Synopsis -*signal-cli* [--config CONFIG] [-h | -v | -a ACCOUNT | --dbus | --dbus-system] command [command-options] +*signal-cli* [--data-dir DATA_DIR] [-h | -v | -a ACCOUNT | --dbus | --dbus-system] command [command-options] == Description @@ -57,8 +57,8 @@ If `--verbose` is also given, the detailed logs will only be written to the log Scrub possibly sensitive information from the log, like phone numbers and UUIDs. Doesn't work reliably on dbus logs with very verbose logging (`-vvv`) -*--config* CONFIG:: -Set the path, where to store the config. +*-d* DATA_DIR, *--data-dir* DATA_DIR, *-c* CONFIG, *--config* CONFIG:: +Set the path where to store account data and local configuration. Make sure you have full read/write access to the given directory. (Default: `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`)) @@ -1172,10 +1172,25 @@ signal-cli -a ACCOUNT trust -a RECIPIENT == Files -The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--config*: +The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--data-dir* (legacy *--config*): `$XDG_DATA_HOME/signal-cli/` (`$HOME/.local/share/signal-cli/`) +=== Configuration file + +signal-cli supports a JSON-based global configuration file that provides defaults for CLI options. +Keys use camelCase and generally match the long CLI parameter names (for example `dataDir`, `verbose`, `logFile`, `serviceEnvironment`, `trustNewIdentities`, `output`, `disableSendLog`, `account`). + +Configuration files are read and merged in this order; later files override earlier ones: + +- `/etc/signal-cli/config.json` (system-wide defaults) +- the path in the `SIGNAL_CLI_CONFIG` environment variable (if set) +- `$XDG_CONFIG_HOME/signal-cli/config.json` (per-user; defaults to `$HOME/.config/signal-cli/config.json`) + +When multiple configuration files are present their settings are merged; values from later files override earlier values. +Command-line options always take precedence over configuration file values. +Overall precedence (highest → lowest): command-line options → per-user config → system config → built-in defaults. + == Authors Maintained by AsamK , who is assisted by other open source contributors. diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index bcf3c605..d29cc2f8 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -46,7 +46,9 @@ public class App { private final Namespace ns; - static ArgumentParser buildArgumentParser() { + static ArgumentParser buildArgumentParser(GlobalConfig config) { + final var cfg = config == null ? GlobalConfig.DEFAULT : config; + var parser = ArgumentParsers.newFor("signal-cli", VERSION_0_9_0_DEFAULT_SETTINGS) .includeArgumentNamesAsKeysInResult(true) .build() @@ -57,47 +59,60 @@ public class App { parser.addArgument("--version").help("Show package version.").action(Arguments.version()); parser.addArgument("-v", "--verbose") .help("Raise log level and include lib signal logs. Specify multiple times for even more logs.") - .action(Arguments.count()); + .action(Arguments.count()) + .setDefault(cfg.verbose()); parser.addArgument("--log-file") .type(File.class) - .help("Write log output to the given file. If --verbose is also given, the detailed logs will only be written to the log file."); + .help("Write log output to the given file. If --verbose is also given, the detailed logs will only be written to the log file.") + .setDefault(cfg.logFile() == null ? null : new File(cfg.logFile())); parser.addArgument("--scrub-log") .action(Arguments.storeTrue()) - .help("Scrub possibly sensitive information from the log, like phone numbers and UUIDs."); - parser.addArgument("-c", "--config") - .help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli)."); + .help("Scrub possibly sensitive information from the log, like phone numbers and UUIDs.") + .setDefault(cfg.scrubLog()); + parser.addArgument("-d", "--data-dir", "-c", "--config") + .help("Set the path where to store data (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).") + .setDefault(cfg.dataDir()); parser.addArgument("-a", "--account", "-u", "--username") .help("Specify your phone number, that will be your identifier."); var mut = parser.addMutuallyExclusiveGroup(); - mut.addArgument("--dbus").dest("global-dbus").help("Make request via user dbus.").action(Arguments.storeTrue()); + mut.addArgument("--dbus") + .dest("global-dbus") + .help("Make request via user dbus.") + .action(Arguments.storeTrue()) + .setDefault(cfg.dbus()); mut.addArgument("--dbus-system") .dest("global-dbus-system") .help("Make request via system dbus.") - .action(Arguments.storeTrue()); + .action(Arguments.storeTrue()) + .setDefault(cfg.dbusSystem()); parser.addArgument("--bus-name") .dest("global-bus-name") - .setDefault(DbusConfig.getBusname()) + .setDefault(cfg.busName() != null ? cfg.busName() : DbusConfig.getBusname()) .help("Specify the D-Bus bus name to connect to."); parser.addArgument("-o", "--output") .help("Choose to output in plain text or JSON") - .type(Arguments.enumStringType(OutputType.class)); + .type(Arguments.enumStringType(OutputType.class)) + .setDefault(cfg.output() == null ? null : cfg.output()); parser.addArgument("--service-environment") .help("Choose the server environment to use.") .type(Arguments.enumStringType(ServiceEnvironmentCli.class)) - .setDefault(ServiceEnvironmentCli.LIVE); + .setDefault(cfg.serviceEnvironment() != null ? cfg.serviceEnvironment() : ServiceEnvironmentCli.LIVE); parser.addArgument("--trust-new-identities") .help("Choose when to trust new identities.") .type(Arguments.enumStringType(TrustNewIdentityCli.class)) - .setDefault(TrustNewIdentityCli.ON_FIRST_USE); + .setDefault(cfg.trustNewIdentities() != null + ? cfg.trustNewIdentities() + : TrustNewIdentityCli.ON_FIRST_USE); parser.addArgument("--disable-send-log") .help("Disable message send log (for resending messages that recipient couldn't decrypt)") - .action(Arguments.storeTrue()); + .action(Arguments.storeTrue()) + .setDefault(cfg.disableSendLog()); parser.epilog( "The global arguments are shown with 'signal-cli -h' and need to come before the subcommand, while the subcommand-specific arguments (shown with 'signal-cli SUBCOMMAND -h') need to be given after the subcommand."); @@ -219,12 +234,12 @@ public class App { } private SignalAccountFiles loadSignalAccountFiles() throws IOErrorException { - final File configPath; - final var config = ns.getString("config"); - if (config != null) { - configPath = new File(config); + final File dataPath; + final var dataDir = ns.getString("data-dir"); + if (dataDir != null) { + dataPath = new File(dataDir); } else { - configPath = getDefaultConfigPath(); + dataPath = getDefaultDataPath(); } final var serviceEnvironmentCli = ns.get("service-environment"); @@ -240,7 +255,7 @@ public class App { final var disableSendLog = Boolean.TRUE.equals(ns.getBoolean("disable-send-log")); try { - return new SignalAccountFiles(configPath, + return new SignalAccountFiles(dataPath, serviceEnvironment, BaseConfig.USER_AGENT, new Settings(trustNewIdentity, disableSendLog)); @@ -339,7 +354,7 @@ public class App { /** * @return the default data directory to be used by signal-cli. */ - private static File getDefaultConfigPath() { + private static File getDefaultDataPath() { return new File(IOUtils.getDataHomeDir(), "signal-cli"); } } diff --git a/src/main/java/org/asamk/signal/ConfigLoader.java b/src/main/java/org/asamk/signal/ConfigLoader.java new file mode 100644 index 00000000..861de2e0 --- /dev/null +++ b/src/main/java/org/asamk/signal/ConfigLoader.java @@ -0,0 +1,81 @@ +package org.asamk.signal; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.asamk.signal.commands.exceptions.UserErrorException; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Loads and merges configuration files. Merge order (later files override earlier): + * - /etc/signal-cli/config.json + * - file pointed to by SIGNAL_CLI_CONFIG (if set) + * - $XDG_CONFIG_HOME/signal-cli/config.json or $HOME/.config/signal-cli/config.json + */ +public final class ConfigLoader { + + private ConfigLoader() { + } + + public static GlobalConfig load() throws UserErrorException { + final ObjectMapper mapper = new ObjectMapper(); + final ObjectNode merged = mapper.createObjectNode(); + + // System config + addIfExists(merged, mapper, Paths.get("/etc/signal-cli/config.json")); + + // User config via env (if set) else XDG or ~/.config + final String env = System.getenv("SIGNAL_CLI_CONFIG"); + if (env != null && !env.isEmpty()) { + addIfExists(merged, mapper, Paths.get(env)); + } else { + final String xdg = System.getenv("XDG_CONFIG_HOME"); + if (xdg != null && !xdg.isEmpty()) { + addIfExists(merged, mapper, Paths.get(xdg, "signal-cli", "config.json")); + } else { + addIfExists(merged, + mapper, + Paths.get(System.getProperty("user.home"), ".config", "signal-cli", "config.json")); + } + } + + try { + if (merged.isEmpty()) { + return GlobalConfig.DEFAULT; + } + return mapper.treeToValue(merged, GlobalConfig.class); + } catch (Exception e) { + throw new UserErrorException("Failed to parse configuration file(s): " + e.getMessage(), e); + } + } + + private static void addIfExists(ObjectNode merged, ObjectMapper mapper, Path p) throws UserErrorException { + if (p == null) return; + try { + if (Files.exists(p)) { + final JsonNode node = mapper.readTree(p.toFile()); + merge(merged, node); + } + } catch (IOException e) { + throw new UserErrorException("Failed to load config from " + p + ": " + e.getMessage(), e); + } + } + + private static void merge(ObjectNode target, JsonNode source) { + source.properties().forEach(entry -> { + final String name = entry.getKey(); + final JsonNode value = entry.getValue(); + final JsonNode existing = target.get(name); + if (existing != null && existing.isObject() && value.isObject()) { + merge((ObjectNode) existing, value); + } else { + target.set(name, value); + } + }); + } +} diff --git a/src/main/java/org/asamk/signal/GlobalConfig.java b/src/main/java/org/asamk/signal/GlobalConfig.java new file mode 100644 index 00000000..1205df7c --- /dev/null +++ b/src/main/java/org/asamk/signal/GlobalConfig.java @@ -0,0 +1,36 @@ +package org.asamk.signal; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record GlobalConfig( + @JsonProperty("verbose") Integer verbose, + @JsonProperty("logFile") String logFile, + @JsonProperty("scrubLog") Boolean scrubLog, + @JsonProperty("dataDir") String dataDir, + @JsonProperty("busName") String busName, + @JsonProperty("dbus") Boolean dbus, + @JsonProperty("dbusSystem") Boolean dbusSystem, + @JsonProperty("output") OutputType output, + @JsonProperty("serviceEnvironment") ServiceEnvironmentCli serviceEnvironment, + @JsonProperty("trustNewIdentities") TrustNewIdentityCli trustNewIdentities, + @JsonProperty("disableSendLog") Boolean disableSendLog, + @JsonProperty("account") String account +) { + + public static final GlobalConfig DEFAULT = new GlobalConfig(null, + null, + null, + null, + null, + null, + null, + null, + ServiceEnvironmentCli.LIVE, + TrustNewIdentityCli.ON_FIRST_USE, + null, + null); + + public static GlobalConfig empty() { + return new GlobalConfig(null, null, null, null, null, null, null, null, null, null, null, null); + } +} diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 568f58c6..800b0ee6 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -40,27 +40,32 @@ import java.security.Security; public class Main { - public static void main(String[] args) { + static void main(String[] args) { // enable unlimited strength crypto via Policy, supported on relevant JREs Security.setProperty("crypto.policy", "unlimited"); installSecurityProviderWorkaround(); + // Load global config early so we can use its values as parser defaults + final GlobalConfig globalConfig; + try { + globalConfig = ConfigLoader.load(); + } catch (UserErrorException e) { + System.exit(handleCommandException(e, null)); + return; + } + // Configuring the logger needs to happen before any logger is initialized - final var loggingConfig = parseLoggingConfig(args); + final var loggingConfig = parseLoggingConfig(args, globalConfig); configureLogging(loggingConfig); - final var parser = App.buildArgumentParser(); + final var parser = App.buildArgumentParser(globalConfig); final var ns = parser.parseArgsOrFail(args); int status = 0; try { new App(ns).init(); } catch (CommandException e) { - System.err.println(e.getMessage()); - if (loggingConfig.verboseLevel > 0 && e.getCause() != null) { - e.getCause().printStackTrace(System.err); - } - status = getStatusForError(e); + status = handleCommandException(e, loggingConfig); } catch (Throwable e) { e.printStackTrace(System.err); status = 2; @@ -69,16 +74,27 @@ public class Main { System.exit(status); } + private static int handleCommandException(final CommandException e, final LoggingConfig loggingConfig) { + System.err.println(e.getMessage()); + if (loggingConfig != null && loggingConfig.verboseLevel > 0 && e.getCause() != null) { + e.getCause().printStackTrace(System.err); + } + return getStatusForError(e); + } + private static void installSecurityProviderWorkaround() { // Register our own security provider Security.insertProviderAt(new SecurityProvider(), 1); Security.addProvider(new BouncyCastleProvider()); } - private static LoggingConfig parseLoggingConfig(final String[] args) { - final var nsLog = parseArgs(args); + private static LoggingConfig parseLoggingConfig(final String[] args, final GlobalConfig config) { + final var nsLog = parseArgs(args, config); if (nsLog == null) { - return new LoggingConfig(0, null, false); + final var verbose = config != null && config.verbose() != null ? config.verbose() : 0; + final var logFile = config != null && config.logFile() != null ? new File(config.logFile()) : null; + final var scrubLog = config != null && Boolean.TRUE.equals(config.scrubLog()); + return new LoggingConfig(verbose, logFile, scrubLog); } final var verboseLevel = nsLog.getInt("verbose"); @@ -90,14 +106,20 @@ public class Main { /** * This method only parses commandline args relevant for logging configuration. */ - private static Namespace parseArgs(String[] args) { + private static Namespace parseArgs(String[] args, final GlobalConfig config) { var parser = ArgumentParsers.newFor("signal-cli", DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS) .includeArgumentNamesAsKeysInResult(true) .build() .defaultHelp(false); - parser.addArgument("-v", "--verbose").action(Arguments.count()); - parser.addArgument("--log-file").type(File.class); - parser.addArgument("--scrub-log").action(Arguments.storeTrue()); + parser.addArgument("-v", "--verbose") + .action(Arguments.count()) + .setDefault(config == null ? null : config.verbose()); + parser.addArgument("--log-file") + .type(File.class) + .setDefault(config == null || config.logFile() == null ? null : new File(config.logFile())); + parser.addArgument("--scrub-log") + .action(Arguments.storeTrue()) + .setDefault(config == null ? null : config.scrubLog()); try { return parser.parseKnownArgs(args, null); @@ -124,12 +146,12 @@ public class Main { private static int getStatusForError(final CommandException e) { return switch (e) { - case UserErrorException userErrorException -> 1; - case UnexpectedErrorException unexpectedErrorException -> 2; - case IOErrorException ioErrorException -> 3; - case UntrustedKeyErrorException untrustedKeyErrorException -> 4; - case RateLimitErrorException rateLimitErrorException -> 5; - case CaptchaRejectedErrorException captchaRejectedErrorException -> 6; + case UserErrorException _ -> 1; + case UnexpectedErrorException _ -> 2; + case IOErrorException _ -> 3; + case UntrustedKeyErrorException _ -> 4; + case RateLimitErrorException _ -> 5; + case CaptchaRejectedErrorException _ -> 6; case null -> 2; }; } diff --git a/src/main/java/org/asamk/signal/OutputType.java b/src/main/java/org/asamk/signal/OutputType.java index 383d635f..2da6b5db 100644 --- a/src/main/java/org/asamk/signal/OutputType.java +++ b/src/main/java/org/asamk/signal/OutputType.java @@ -1,5 +1,7 @@ package org.asamk.signal; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum OutputType { PLAIN_TEXT { @Override @@ -12,5 +14,16 @@ public enum OutputType { public String toString() { return "json"; } - }, + }; + + @JsonCreator + public static OutputType fromString(String value) { + if (value == null) return null; + final var norm = value.trim().toLowerCase().replaceAll("[^a-z0-9]", ""); + return switch (norm) { + case "plaintext" -> PLAIN_TEXT; + case "json" -> JSON; + default -> throw new IllegalArgumentException("Invalid output type: " + value); + }; + } } diff --git a/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java b/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java index 9010f3f8..f61a6833 100644 --- a/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java +++ b/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java @@ -1,5 +1,7 @@ package org.asamk.signal; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum ServiceEnvironmentCli { LIVE { @Override @@ -12,5 +14,16 @@ public enum ServiceEnvironmentCli { public String toString() { return "staging"; } - }, + }; + + @JsonCreator + public static ServiceEnvironmentCli fromString(String value) { + if (value == null) return null; + final var norm = value.trim().toLowerCase(); + return switch (norm) { + case "live" -> LIVE; + case "staging" -> STAGING; + default -> throw new IllegalArgumentException("Invalid service-environment: " + value); + }; + } } diff --git a/src/main/java/org/asamk/signal/TrustNewIdentityCli.java b/src/main/java/org/asamk/signal/TrustNewIdentityCli.java index 5cc36bbd..665ab90b 100644 --- a/src/main/java/org/asamk/signal/TrustNewIdentityCli.java +++ b/src/main/java/org/asamk/signal/TrustNewIdentityCli.java @@ -1,5 +1,7 @@ package org.asamk.signal; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum TrustNewIdentityCli { ALWAYS { @Override @@ -18,5 +20,17 @@ public enum TrustNewIdentityCli { public String toString() { return "never"; } - }, + }; + + @JsonCreator + public static TrustNewIdentityCli fromString(String value) { + if (value == null) return null; + final var norm = value.trim().toLowerCase().replaceAll("[^a-z0-9]", ""); + return switch (norm) { + case "always" -> ALWAYS; + case "onfirstuse" -> ON_FIRST_USE; + case "never" -> NEVER; + default -> throw new IllegalArgumentException("Invalid trust-new-identities: " + value); + }; + } } diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json index 4513328b..cb971b26 100644 --- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json @@ -1317,6 +1317,14 @@ } ] }, + { + "type": "java.util.concurrent.CopyOnWriteArrayList", + "fields": [ + { + "name": "lock" + } + ] + }, { "type": "java.util.concurrent.ForkJoinTask", "fields": [ @@ -1979,6 +1987,28 @@ { "type": "org.asamk.SignalControl.Error.InvalidNumber" }, + { + "type": "org.asamk.signal.GlobalConfig", + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.Integer", + "java.lang.String", + "java.lang.Boolean", + "java.lang.String", + "java.lang.String", + "java.lang.Boolean", + "java.lang.Boolean", + "org.asamk.signal.OutputType", + "org.asamk.signal.ServiceEnvironmentCli", + "org.asamk.signal.TrustNewIdentityCli", + "java.lang.Boolean", + "java.lang.String" + ] + } + ] + }, { "type": "org.asamk.signal.Main", "jniAccessible": true, @@ -1995,6 +2025,31 @@ } ] }, + { + "type": "org.asamk.signal.OutputType" + }, + { + "type": "org.asamk.signal.ServiceEnvironmentCli", + "methods": [ + { + "name": "fromString", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "type": "org.asamk.signal.TrustNewIdentityCli", + "methods": [ + { + "name": "fromString", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, { "type": "org.asamk.signal.commands.AcceptCallCommand$JsonCallInfo", "allDeclaredFields": true,