From: Atomic-Bean <75401809+Atomic-Bean@users.noreply.github.com> Date: Wed, 13 Jan 2021 15:51:31 +0000 (+1030) Subject: Added JSON output to listGroups and allowed json to be activated with -j (#401) X-Git-Tag: v0.7.3~39 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/90f5cd79c982c9d9f5f5103ca9af4ee289ee9ba1 Added JSON output to listGroups and allowed json to be activated with -j (#401) * Added json output to listGroups and allowed json to be activated with -j * Renamed classes * Added AsamK's suggestions * Removed isRegisted check since that is causing a conflict with upstream * Fixed an issue in the help message for listGroupsCommand * Re-enabled --json for receive and getUserStatuses commands as deprecated * Added better depricated warning message and clarified some java doc stuff --- diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index fa2db7c3..ba0d72d4 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -44,6 +44,9 @@ Make request via user dbus. *--dbus-system*:: Make request via system dbus. +*-o* OUTPUT-MODE, *--output* OUTPUT-MODE:: +Specify if you want commands to output in either "plain-text" mode or in "json". Defaults to "plain-text" + == Commands === register @@ -126,12 +129,10 @@ Use listDevices to see the deviceIds. === getUserStatus -Uses a list of phone numbers to determine the statuses of those users. Shows if they are registered on the Signal Servers or not. +Uses a list of phone numbers to determine the statuses of those users. Shows if they are registered on the Signal Servers or not. In json mode this is outputted as a list of objects. [NUMBER [NUMBER ...]]:: One or more numbers to check. -*--json*:: -Output the statuses as an array of json objects. === send @@ -177,15 +178,13 @@ Remove a reaction. === receive Query the server for new messages. -New messages are printed on standardoutput and attachments are downloaded to the config directory. +New messages are printed on standard output and attachments are downloaded to the config directory. In json mode this is outputted as one json object per line. *-t* TIMEOUT, *--timeout* TIMEOUT:: Number of seconds to wait for new messages (negative values disable timeout). Default is 5 seconds. *--ignore-attachments*:: Don’t download attachments of received messages. -*--json*:: -Output received messages in json format, one object per line. === joinGroup @@ -222,10 +221,10 @@ Specify the recipient group ID in base64 encoding. === listGroups -Show a list of known groups. +Show a list of known groups and related information. In json mode this is outputted as an list of objects and is always in detailed mode. *-d*, *--detailed*:: -Include the list of members of each group. +Include the list of members of each group and the group invite link. === listIdentities diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 1eb36b2a..b90b7cba 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -299,6 +299,9 @@ public class Main { mut.addArgument("--dbus").help("Make request via user dbus.").action(Arguments.storeTrue()); mut.addArgument("--dbus-system").help("Make request via system dbus.").action(Arguments.storeTrue()); + parser.addArgument("-o", "--output").help("Choose to output in plain text or JSON") + .choices("plain-text", "json").setDefault("plain-text"); + Subparsers subparsers = parser.addSubparsers() .title("subcommands") .dest("command") diff --git a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java index 0a1ddc4c..8078de10 100644 --- a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java +++ b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java @@ -8,6 +8,8 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.HashSet; @@ -17,12 +19,15 @@ import java.util.stream.Collectors; public class GetUserStatusCommand implements LocalCommand { + // TODO delete later when "json" variable is removed + final static Logger logger = LoggerFactory.getLogger(GetUserStatusCommand.class); + @Override public void attachToSubparser(final Subparser subparser) { subparser.addArgument("number").help("Phone number").nargs("+"); subparser.help("Check if the specified phone number/s have been registered"); subparser.addArgument("--json") - .help("Output received messages in json format, one json object per line.") + .help("WARNING: This parameter is now deprecated! Please use the \"output\" option instead.\n\nOutput received messages in json format, one json object per line.") .action(Arguments.storeTrue()); } @@ -32,6 +37,13 @@ public class GetUserStatusCommand implements LocalCommand { ObjectMapper jsonProcessor = new ObjectMapper(); jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + boolean inJson = ns.getString("output").equals("json"); + + // TODO delete later when "json" variable is removed + if (ns.getBoolean("json")) { + logger.warn("\"--json\" option has been deprecated, please use \"output\" instead."); + } + // Get a map of registration statuses Map registered; try { @@ -42,7 +54,7 @@ public class GetUserStatusCommand implements LocalCommand { } // Output - if (ns.getBoolean("json")) { + if (inJson) { List objects = registered.entrySet() .stream() .map(entry -> new JsonIsRegistered(entry.getKey(), entry.getValue())) diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index 97af502e..debc2a42 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -9,32 +9,39 @@ import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.storage.groups.GroupInfo; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +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 java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.stream.Collectors; public class ListGroupsCommand implements LocalCommand { - private static void printGroup(Manager m, GroupInfo group, boolean detailed) { - if (detailed) { - Set members = group.getMembers() - .stream() - .map(m::resolveSignalServiceAddress) - .map(SignalServiceAddress::getLegacyIdentifier) - .collect(Collectors.toSet()); - - Set pendingMembers = group.getPendingMembers() - .stream() - .map(m::resolveSignalServiceAddress) - .map(SignalServiceAddress::getLegacyIdentifier) - .collect(Collectors.toSet()); - - Set requestingMembers = group.getRequestingMembers() - .stream() - .map(m::resolveSignalServiceAddress) - .map(SignalServiceAddress::getLegacyIdentifier) - .collect(Collectors.toSet()); + private static Set resolveMembers(Manager m, Set addresses) { + return addresses.stream().map(m::resolveSignalServiceAddress) + .map(SignalServiceAddress::getLegacyIdentifier) + .collect(Collectors.toSet()); + } + + private static int printGroupsJson(ObjectMapper jsonProcessor, List objects) { + try { + jsonProcessor.writeValue(System.out, objects); + System.out.println(); + } catch (IOException e) { + System.err.println(e.getMessage()); + return 1; + } + return 0; + } + + private static void printGroupPlainText(Manager m, GroupInfo group, boolean detailed) { + if (detailed) { final GroupInviteLinkUrl groupInviteLink = group.getGroupInviteLink(); System.out.println(String.format( @@ -43,9 +50,9 @@ public class ListGroupsCommand implements LocalCommand { group.getTitle(), group.isMember(m.getSelfAddress()), group.isBlocked(), - members, - pendingMembers, - requestingMembers, + resolveMembers(m, group.getMembers()), + resolveMembers(m, group.getPendingMembers()), + resolveMembers(m, group.getRequestingMembers()), groupInviteLink == null ? '-' : groupInviteLink.getUrl())); } else { System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b", @@ -58,18 +65,68 @@ public class ListGroupsCommand implements LocalCommand { @Override public void attachToSubparser(final Subparser subparser) { - subparser.addArgument("-d", "--detailed").action(Arguments.storeTrue()).help("List members of each group"); - subparser.help("List group name and ids"); + subparser.addArgument("-d", "--detailed").action(Arguments.storeTrue()) + .help("List the members and group invite links of each group. If output=json, then this is always set"); + + subparser.help("List group information including names, ids, active status, blocked status and members"); } @Override public int handleCommand(final Namespace ns, final Manager m) { - List groups = m.getGroups(); - boolean detailed = ns.getBoolean("detailed"); + if (ns.getString("output").equals("json")) { + final ObjectMapper jsonProcessor = new ObjectMapper(); + jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); - for (GroupInfo group : groups) { - printGroup(m, group, detailed); + List objects = new ArrayList<>(); + for (GroupInfo group : m.getGroups()) { + final GroupInviteLinkUrl groupInviteLink = group.getGroupInviteLink(); + + objects.add(new JsonGroup(group.getGroupId().toBase64(), + group.getTitle(), + group.isMember(m.getSelfAddress()), + group.isBlocked(), + resolveMembers(m, group.getMembers()), + resolveMembers(m, group.getPendingMembers()), + resolveMembers(m, group.getRequestingMembers()), + groupInviteLink == null ? null : groupInviteLink.getUrl())); + } + return printGroupsJson(jsonProcessor, objects); + } else { + boolean detailed = ns.getBoolean("detailed"); + for (GroupInfo group : m.getGroups()) { + printGroupPlainText(m, group, detailed); + } } + return 0; } + + private static final class JsonGroup { + + public String id; + public String name; + public boolean isMember; + public boolean isBlocked; + + public Set members; + public Set pendingMembers; + public Set requestingMembers; + public String groupInviteLink; + + public JsonGroup(String id, String name, boolean isMember, boolean isBlocked, + Set members, Set pendingMembers, + Set requestingMembers, String groupInviteLink) + { + this.id = id; + this.name = name; + this.isMember = isMember; + this.isBlocked = isBlocked; + + this.members = members; + this.pendingMembers = pendingMembers; + this.requestingMembers = requestingMembers; + this.groupInviteLink = groupInviteLink; + } + } } diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index 7dc9dcaf..f7a49157 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -20,6 +20,10 @@ import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.whispersystems.util.Base64; +// TODO delete later when "json" variable is removed +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.util.concurrent.TimeUnit; @@ -27,6 +31,9 @@ import static org.asamk.signal.util.ErrorUtils.handleAssertionError; public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { + // TODO delete later when "json" variable is removed + final static Logger logger = LoggerFactory.getLogger(ReceiveCommand.class); + @Override public void attachToSubparser(final Subparser subparser) { subparser.addArgument("-t", "--timeout") @@ -36,13 +43,21 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { .help("Don’t download attachments of received messages.") .action(Arguments.storeTrue()); subparser.addArgument("--json") - .help("Output received messages in json format, one json object per line.") + .help("WARNING: This parameter is now deprecated! Please use the \"output\" option instead.\n\nOutput received messages in json format, one json object per line.") .action(Arguments.storeTrue()); } public int handleCommand(final Namespace ns, final Signal signal, DBusConnection dbusconnection) { final ObjectMapper jsonProcessor; + + boolean inJson = ns.getString("output").equals("json") || ns.getBoolean("json"); + + // TODO delete later when "json" variable is removed if (ns.getBoolean("json")) { + logger.warn("\"--json\" option has been deprecated, please use \"output\" instead."); + } + + if (inJson) { jsonProcessor = new ObjectMapper(); jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); @@ -146,6 +161,13 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { + boolean inJson = ns.getString("output").equals("json") || ns.getBoolean("json"); + + // TODO delete later when "json" variable is removed + if (ns.getBoolean("json")) { + logger.warn("\"--json\" option has been deprecated, please use \"output\" instead."); + } + double timeout = 5; if (ns.getDouble("timeout") != null) { timeout = ns.getDouble("timeout"); @@ -157,7 +179,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { } boolean ignoreAttachments = ns.getBoolean("ignore_attachments"); try { - final Manager.ReceiveMessageHandler handler = ns.getBoolean("json") + final Manager.ReceiveMessageHandler handler = inJson ? new JsonReceiveMessageHandler(m) : new ReceiveMessageHandler(m); m.receiveMessages((long) (timeout * 1000), diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 0c7a5e00..c3e7a638 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -312,7 +312,7 @@ public class Manager implements Closeable { * * @param numbers The set of phone number in question * @return A map of numbers to booleans. True if registered, false otherwise. Should never be null - * @throws IOException if its unable to check if the users are registered + * @throws IOException if its unable to get the contacts to check if they're registered */ public Map areUsersRegistered(Set numbers) throws IOException { // Note "contactDetails" has no optionals. It only gives us info on users who are registered