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
This commit is contained in:
Atomic-Bean 2021-01-14 02:21:31 +10:30 committed by GitHub
parent c9fa28d844
commit 90f5cd79c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 134 additions and 41 deletions

View file

@ -44,6 +44,9 @@ Make request via user dbus.
*--dbus-system*:: *--dbus-system*::
Make request via system dbus. 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 == Commands
=== register === register
@ -126,12 +129,10 @@ Use listDevices to see the deviceIds.
=== getUserStatus === 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 ...]]:: [NUMBER [NUMBER ...]]::
One or more numbers to check. One or more numbers to check.
*--json*::
Output the statuses as an array of json objects.
=== send === send
@ -177,15 +178,13 @@ Remove a reaction.
=== receive === receive
Query the server for new messages. 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:: *-t* TIMEOUT, *--timeout* TIMEOUT::
Number of seconds to wait for new messages (negative values disable timeout). Number of seconds to wait for new messages (negative values disable timeout).
Default is 5 seconds. Default is 5 seconds.
*--ignore-attachments*:: *--ignore-attachments*::
Dont download attachments of received messages. Dont download attachments of received messages.
*--json*::
Output received messages in json format, one object per line.
=== joinGroup === joinGroup
@ -222,10 +221,10 @@ Specify the recipient group ID in base64 encoding.
=== listGroups === 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*:: *-d*, *--detailed*::
Include the list of members of each group. Include the list of members of each group and the group invite link.
=== listIdentities === listIdentities

View file

@ -299,6 +299,9 @@ public class Main {
mut.addArgument("--dbus").help("Make request via user dbus.").action(Arguments.storeTrue()); 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()); 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() Subparsers subparsers = parser.addSubparsers()
.title("subcommands") .title("subcommands")
.dest("command") .dest("command")

View file

@ -8,6 +8,8 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.util.HashSet; import java.util.HashSet;
@ -17,12 +19,15 @@ import java.util.stream.Collectors;
public class GetUserStatusCommand implements LocalCommand { public class GetUserStatusCommand implements LocalCommand {
// TODO delete later when "json" variable is removed
final static Logger logger = LoggerFactory.getLogger(GetUserStatusCommand.class);
@Override @Override
public void attachToSubparser(final Subparser subparser) { public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("number").help("Phone number").nargs("+"); subparser.addArgument("number").help("Phone number").nargs("+");
subparser.help("Check if the specified phone number/s have been registered"); subparser.help("Check if the specified phone number/s have been registered");
subparser.addArgument("--json") 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()); .action(Arguments.storeTrue());
} }
@ -32,6 +37,13 @@ public class GetUserStatusCommand implements LocalCommand {
ObjectMapper jsonProcessor = new ObjectMapper(); ObjectMapper jsonProcessor = new ObjectMapper();
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); 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 // Get a map of registration statuses
Map<String, Boolean> registered; Map<String, Boolean> registered;
try { try {
@ -42,7 +54,7 @@ public class GetUserStatusCommand implements LocalCommand {
} }
// Output // Output
if (ns.getBoolean("json")) { if (inJson) {
List<JsonIsRegistered> objects = registered.entrySet() List<JsonIsRegistered> objects = registered.entrySet()
.stream() .stream()
.map(entry -> new JsonIsRegistered(entry.getKey(), entry.getValue())) .map(entry -> new JsonIsRegistered(entry.getKey(), entry.getValue()))

View file

@ -9,32 +9,39 @@ import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; 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.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class ListGroupsCommand implements LocalCommand { public class ListGroupsCommand implements LocalCommand {
private static void printGroup(Manager m, GroupInfo group, boolean detailed) { private static Set<String> resolveMembers(Manager m, Set<SignalServiceAddress> 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) { if (detailed) {
Set<String> members = group.getMembers()
.stream()
.map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
Set<String> pendingMembers = group.getPendingMembers()
.stream()
.map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
Set<String> requestingMembers = group.getRequestingMembers()
.stream()
.map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
final GroupInviteLinkUrl groupInviteLink = group.getGroupInviteLink(); final GroupInviteLinkUrl groupInviteLink = group.getGroupInviteLink();
System.out.println(String.format( System.out.println(String.format(
@ -43,9 +50,9 @@ public class ListGroupsCommand implements LocalCommand {
group.getTitle(), group.getTitle(),
group.isMember(m.getSelfAddress()), group.isMember(m.getSelfAddress()),
group.isBlocked(), group.isBlocked(),
members, resolveMembers(m, group.getMembers()),
pendingMembers, resolveMembers(m, group.getPendingMembers()),
requestingMembers, resolveMembers(m, group.getRequestingMembers()),
groupInviteLink == null ? '-' : groupInviteLink.getUrl())); groupInviteLink == null ? '-' : groupInviteLink.getUrl()));
} else { } else {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b", System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b",
@ -58,18 +65,68 @@ public class ListGroupsCommand implements LocalCommand {
@Override @Override
public void attachToSubparser(final Subparser subparser) { public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-d", "--detailed").action(Arguments.storeTrue()).help("List members of each group"); subparser.addArgument("-d", "--detailed").action(Arguments.storeTrue())
subparser.help("List group name and ids"); .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 @Override
public int handleCommand(final Namespace ns, final Manager m) { public int handleCommand(final Namespace ns, final Manager m) {
List<GroupInfo> groups = m.getGroups(); if (ns.getString("output").equals("json")) {
boolean detailed = ns.getBoolean("detailed"); final ObjectMapper jsonProcessor = new ObjectMapper();
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
for (GroupInfo group : groups) { List<JsonGroup> objects = new ArrayList<>();
printGroup(m, group, detailed); 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; return 0;
} }
private static final class JsonGroup {
public String id;
public String name;
public boolean isMember;
public boolean isBlocked;
public Set<String> members;
public Set<String> pendingMembers;
public Set<String> requestingMembers;
public String groupInviteLink;
public JsonGroup(String id, String name, boolean isMember, boolean isBlocked,
Set<String> members, Set<String> pendingMembers,
Set<String> 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;
}
}
} }

View file

@ -20,6 +20,10 @@ import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.util.Base64; 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.io.IOException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -27,6 +31,9 @@ import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
// TODO delete later when "json" variable is removed
final static Logger logger = LoggerFactory.getLogger(ReceiveCommand.class);
@Override @Override
public void attachToSubparser(final Subparser subparser) { public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-t", "--timeout") subparser.addArgument("-t", "--timeout")
@ -36,13 +43,21 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
.help("Dont download attachments of received messages.") .help("Dont download attachments of received messages.")
.action(Arguments.storeTrue()); .action(Arguments.storeTrue());
subparser.addArgument("--json") 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()); .action(Arguments.storeTrue());
} }
public int handleCommand(final Namespace ns, final Signal signal, DBusConnection dbusconnection) { public int handleCommand(final Namespace ns, final Signal signal, DBusConnection dbusconnection) {
final ObjectMapper jsonProcessor; 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")) { if (ns.getBoolean("json")) {
logger.warn("\"--json\" option has been deprecated, please use \"output\" instead.");
}
if (inJson) {
jsonProcessor = new ObjectMapper(); jsonProcessor = new ObjectMapper();
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
@ -146,6 +161,13 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
@Override @Override
public int handleCommand(final Namespace ns, final Manager m) { 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; double timeout = 5;
if (ns.getDouble("timeout") != null) { if (ns.getDouble("timeout") != null) {
timeout = ns.getDouble("timeout"); timeout = ns.getDouble("timeout");
@ -157,7 +179,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
} }
boolean ignoreAttachments = ns.getBoolean("ignore_attachments"); boolean ignoreAttachments = ns.getBoolean("ignore_attachments");
try { try {
final Manager.ReceiveMessageHandler handler = ns.getBoolean("json") final Manager.ReceiveMessageHandler handler = inJson
? new JsonReceiveMessageHandler(m) ? new JsonReceiveMessageHandler(m)
: new ReceiveMessageHandler(m); : new ReceiveMessageHandler(m);
m.receiveMessages((long) (timeout * 1000), m.receiveMessages((long) (timeout * 1000),

View file

@ -312,7 +312,7 @@ public class Manager implements Closeable {
* *
* @param numbers The set of phone number in question * @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 * @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<String, Boolean> areUsersRegistered(Set<String> numbers) throws IOException { public Map<String, Boolean> areUsersRegistered(Set<String> numbers) throws IOException {
// Note "contactDetails" has no optionals. It only gives us info on users who are registered // Note "contactDetails" has no optionals. It only gives us info on users who are registered