Merge remote-tracking branch 'asamk/master'

This commit is contained in:
Julius Bünger 2021-01-15 12:54:30 +01:00
commit 52552c331d
36 changed files with 1212 additions and 796 deletions

1
.gitignore vendored
View file

@ -10,3 +10,4 @@ local.properties
.project
.settings/
out/
.DS_Store

View file

@ -1,6 +1,8 @@
# Changelog
## [Unreleased]
### Added
- `--verbose` flag to increase log level
### Fixed
- Disable registration lock before removing the PIN

View file

@ -21,6 +21,9 @@ For registering you need a phone number where you can receive SMS or incoming ca
signal-cli was primarily developed to be used on servers to notify admins of important events.
For this use-case, it has a dbus interface, that can be used to send messages from any programming language that has dbus bindings.
For some functionality the Signal protocol requires that all messages have been received from the server.
The `receive` command should be regularly executed. In daemon mode messages are continuously received.
== Options
*-h*, *--help*::
@ -29,6 +32,9 @@ Show help message and quit.
*-v*, *--version*::
Print the version and quit.
*--verbose*::
Raise log level and include lib signal logs.
*--config* CONFIG::
Set the path, where to store the config.
Make sure you have full read/write access to the given directory.
@ -44,6 +50,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
@ -97,7 +106,8 @@ Remove the registration lock pin.
=== link
Link to an existing device, instead of registering a new number.
This shows a "tsdevice:/…" URI. If you want to connect to another signal-cli instance, you can just use this URI. If you want to link to an Android/iOS device, create a QR code with the URI (e.g. with qrencode) and scan that in the Signal app.
This shows a "tsdevice:/…" URI. If you want to connect to another signal-cli instance, you can just use this URI.
If you want to link to an Android/iOS device, create a QR code with the URI (e.g. with qrencode) and scan that in the Signal app.
*-n* NAME, *--name* NAME::
Optionally specify a name to describe this new device.
@ -109,7 +119,8 @@ Link another device to this device.
Only works, if this is the master device.
*--uri* URI::
Specify the uri contained in the QR code shown by the new device. You will need the full uri enclosed in quotation marks, such as "tsdevice:/?uuid=....."
Specify the uri contained in the QR code shown by the new device.
You will need the full uri enclosed in quotation marks, such as "tsdevice:/?uuid=....."
=== listDevices
@ -126,12 +137,12 @@ 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 +188,14 @@ 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*::
Dont download attachments of received messages.
*--json*::
Output received messages in json format, one object per line.
=== joinGroup
@ -222,10 +232,11 @@ 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

View file

@ -0,0 +1,206 @@
package org.asamk.signal;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.Signal;
import org.asamk.signal.commands.Command;
import org.asamk.signal.commands.Commands;
import org.asamk.signal.commands.DbusCommand;
import org.asamk.signal.commands.ExtendedDbusCommand;
import org.asamk.signal.commands.LocalCommand;
import org.asamk.signal.commands.ProvisioningCommand;
import org.asamk.signal.commands.RegistrationCommand;
import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotRegisteredException;
import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.ServiceConfig;
import org.asamk.signal.util.IOUtils;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import java.io.File;
import java.io.IOException;
public class Cli {
private final static Logger logger = LoggerFactory.getLogger(Main.class);
private final Namespace ns;
public Cli(final Namespace ns) {
this.ns = ns;
}
public int init() {
Command command = getCommand();
if (command == null) {
logger.error("Command not implemented!");
return 2;
}
if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
return initDbusClient(command, ns.getBoolean("dbus_system"));
}
final String username = ns.getString("username");
final File dataPath;
String config = ns.getString("config");
if (config != null) {
dataPath = new File(config);
} else {
dataPath = getDefaultDataPath();
}
final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(
BaseConfig.USER_AGENT);
if (!ServiceConfig.getCapabilities().isGv2()) {
logger.warn("WARNING: Support for new group V2 is disabled,"
+ " because the required native library dependency is missing: libzkgroup");
}
if (username == null) {
ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
return handleCommand(command, pm);
}
if (command instanceof RegistrationCommand) {
final RegistrationManager manager;
try {
manager = RegistrationManager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
} catch (Throwable e) {
logger.error("Error loading or creating state file: {}", e.getMessage());
return 1;
}
try (RegistrationManager m = manager) {
return handleCommand(command, m);
} catch (Exception e) {
logger.error("Cleanup failed", e);
return 2;
}
}
Manager manager;
try {
manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
} catch (NotRegisteredException e) {
System.err.println("User is not registered.");
return 0;
} catch (Throwable e) {
logger.error("Error loading state file: {}", e.getMessage());
return 1;
}
try (Manager m = manager) {
try {
m.checkAccountState();
} catch (IOException e) {
logger.error("Error while checking account: {}", e.getMessage());
return 1;
}
return handleCommand(command, m);
} catch (IOException e) {
logger.error("Cleanup failed", e);
return 2;
}
}
private Command getCommand() {
String commandKey = ns.getString("command");
return Commands.getCommand(commandKey);
}
private int initDbusClient(final Command command, final boolean systemBus) {
try {
DBusConnection.DBusBusType busType;
if (systemBus) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.DBusBusType.SESSION;
}
try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) {
Signal ts = dBusConn.getRemoteObject(DbusConfig.SIGNAL_BUSNAME,
DbusConfig.SIGNAL_OBJECTPATH,
Signal.class);
return handleCommand(command, ts, dBusConn);
}
} catch (DBusException | IOException e) {
logger.error("Dbus client failed", e);
return 2;
}
}
private int handleCommand(Command command, Signal ts, DBusConnection dBusConn) {
if (command instanceof ExtendedDbusCommand) {
return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn);
} else if (command instanceof DbusCommand) {
return ((DbusCommand) command).handleCommand(ns, ts);
} else {
System.err.println("Command is not yet implemented via dbus");
return 1;
}
}
private int handleCommand(Command command, ProvisioningManager pm) {
if (command instanceof ProvisioningCommand) {
return ((ProvisioningCommand) command).handleCommand(ns, pm);
} else {
System.err.println("Command only works with a username");
return 1;
}
}
private int handleCommand(Command command, RegistrationManager m) {
if (command instanceof RegistrationCommand) {
return ((RegistrationCommand) command).handleCommand(ns, m);
}
return 1;
}
private int handleCommand(Command command, Manager m) {
if (command instanceof LocalCommand) {
return ((LocalCommand) command).handleCommand(ns, m);
} else if (command instanceof DbusCommand) {
return ((DbusCommand) command).handleCommand(ns, new DbusSignalImpl(m));
} else {
System.err.println("Command only works via dbus");
return 1;
}
}
/**
* Uses $XDG_DATA_HOME/signal-cli if it exists, or if none of the legacy directories exist:
* - $HOME/.config/signal
* - $HOME/.config/textsecure
*
* @return the data directory to be used by signal-cli.
*/
private static File getDefaultDataPath() {
File dataPath = new File(IOUtils.getDataHomeDir(), "signal-cli");
if (dataPath.exists()) {
return dataPath;
}
File configPath = new File(System.getProperty("user.home"), ".config");
File legacySettingsPath = new File(configPath, "signal");
if (legacySettingsPath.exists()) {
return legacySettingsPath;
}
legacySettingsPath = new File(configPath, "textsecure");
if (legacySettingsPath.exists()) {
return legacySettingsPath;
}
return dataPath;
}
}

View file

@ -25,227 +25,62 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import net.sourceforge.argparse4j.inf.Subparsers;
import org.asamk.Signal;
import org.asamk.signal.commands.Command;
import org.asamk.signal.commands.Commands;
import org.asamk.signal.commands.DbusCommand;
import org.asamk.signal.commands.ExtendedDbusCommand;
import org.asamk.signal.commands.LocalCommand;
import org.asamk.signal.commands.ProvisioningCommand;
import org.asamk.signal.commands.RegistrationCommand;
import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotRegisteredException;
import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.ServiceConfig;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.manager.LibSignalLogger;
import org.asamk.signal.util.SecurityProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import java.io.File;
import java.io.IOException;
import java.security.Security;
import java.util.Map;
public class Main {
final static Logger logger = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
installSecurityProviderWorkaround();
// Configuring the logger needs to happen before any logger is initialized
if (isVerbose(args)) {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace");
System.setProperty("org.slf4j.simpleLogger.showThreadName", "true");
System.setProperty("org.slf4j.simpleLogger.showShortLogName", "false");
System.setProperty("org.slf4j.simpleLogger.showDateTime", "true");
System.setProperty("org.slf4j.simpleLogger.dateTimeFormat", "yyyy-MM-dd'T'HH:mm:ss.SSSXX");
LibSignalLogger.initLogger();
} else {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "info");
System.setProperty("org.slf4j.simpleLogger.showThreadName", "false");
System.setProperty("org.slf4j.simpleLogger.showShortLogName", "true");
System.setProperty("org.slf4j.simpleLogger.showDateTime", "false");
}
Namespace ns = parseArgs(args);
if (ns == null) {
System.exit(1);
}
int res = init(ns);
int res = new Cli(ns).init();
System.exit(res);
}
public static void installSecurityProviderWorkaround() {
private static void installSecurityProviderWorkaround() {
// Register our own security provider
Security.insertProviderAt(new SecurityProvider(), 1);
Security.addProvider(new BouncyCastleProvider());
}
public static int init(Namespace ns) {
Command command = getCommand(ns);
if (command == null) {
logger.error("Command not implemented!");
return 3;
}
private static boolean isVerbose(String[] args) {
ArgumentParser parser = buildBaseArgumentParser();
if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
return initDbusClient(command, ns, ns.getBoolean("dbus_system"));
}
final String username = ns.getString("username");
final File dataPath;
String config = ns.getString("config");
if (config != null) {
dataPath = new File(config);
} else {
dataPath = getDefaultDataPath();
}
final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(
BaseConfig.USER_AGENT);
if (!ServiceConfig.getCapabilities().isGv2()) {
logger.warn("WARNING: Support for new group V2 is disabled,"
+ " because the required native library dependency is missing: libzkgroup");
}
if (username == null) {
ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
return handleCommand(command, ns, pm);
}
if (command instanceof RegistrationCommand) {
final RegistrationManager manager;
Namespace ns;
try {
manager = RegistrationManager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
} catch (Throwable e) {
logger.error("Error loading or creating state file: {}", e.getMessage());
return 2;
}
try (RegistrationManager m = manager) {
return handleCommand(command, ns, m);
} catch (Exception e) {
logger.error("Cleanup failed", e);
return 3;
}
ns = parser.parseKnownArgs(args, null);
} catch (ArgumentParserException e) {
return false;
}
Manager manager;
try {
manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
} catch (NotRegisteredException e) {
System.err.println("User is not registered.");
return 1;
} catch (Throwable e) {
logger.error("Error loading state file: {}", e.getMessage());
return 2;
}
try (Manager m = manager) {
try {
m.checkAccountState();
} catch (IOException e) {
logger.error("Error while checking account: {}", e.getMessage());
return 2;
}
return handleCommand(command, ns, m);
} catch (IOException e) {
logger.error("Cleanup failed", e);
return 3;
}
}
private static int initDbusClient(final Command command, final Namespace ns, final boolean systemBus) {
try {
DBusConnection.DBusBusType busType;
if (systemBus) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.DBusBusType.SESSION;
}
try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) {
Signal ts = dBusConn.getRemoteObject(DbusConfig.SIGNAL_BUSNAME,
DbusConfig.SIGNAL_OBJECTPATH,
Signal.class);
return handleCommand(command, ns, ts, dBusConn);
}
} catch (DBusException | IOException e) {
logger.error("Dbus client failed", e);
return 3;
}
}
private static Command getCommand(Namespace ns) {
String commandKey = ns.getString("command");
final Map<String, Command> commands = Commands.getCommands();
if (!commands.containsKey(commandKey)) {
return null;
}
return commands.get(commandKey);
}
private static int handleCommand(Command command, Namespace ns, Signal ts, DBusConnection dBusConn) {
if (command instanceof ExtendedDbusCommand) {
return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn);
} else if (command instanceof DbusCommand) {
return ((DbusCommand) command).handleCommand(ns, ts);
} else {
System.err.println("Command is not yet implemented via dbus");
return 1;
}
}
private static int handleCommand(Command command, Namespace ns, ProvisioningManager pm) {
if (command instanceof ProvisioningCommand) {
return ((ProvisioningCommand) command).handleCommand(ns, pm);
} else {
System.err.println("Command only works with a username");
return 1;
}
}
private static int handleCommand(Command command, Namespace ns, RegistrationManager m) {
if (command instanceof RegistrationCommand) {
return ((RegistrationCommand) command).handleCommand(ns, m);
}
return 1;
}
private static int handleCommand(Command command, Namespace ns, Manager m) {
if (command instanceof LocalCommand) {
return ((LocalCommand) command).handleCommand(ns, m);
} else if (command instanceof DbusCommand) {
return ((DbusCommand) command).handleCommand(ns, new DbusSignalImpl(m));
} else {
System.err.println("Command only works via dbus");
return 1;
}
}
/**
* Uses $XDG_DATA_HOME/signal-cli if it exists, or if none of the legacy directories exist:
* - $HOME/.config/signal
* - $HOME/.config/textsecure
*
* @return the data directory to be used by signal-cli.
*/
private static File getDefaultDataPath() {
File dataPath = new File(IOUtils.getDataHomeDir(), "signal-cli");
if (dataPath.exists()) {
return dataPath;
}
File configPath = new File(System.getProperty("user.home"), ".config");
File legacySettingsPath = new File(configPath, "signal");
if (legacySettingsPath.exists()) {
return legacySettingsPath;
}
legacySettingsPath = new File(configPath, "textsecure");
if (legacySettingsPath.exists()) {
return legacySettingsPath;
}
return dataPath;
return ns.getBoolean("verbose");
}
private static Namespace parseArgs(String[] args) {
@ -276,30 +111,17 @@ public class Main {
System.exit(2);
}
}
if (ns.getList("recipient") != null && !ns.getList("recipient").isEmpty() && ns.getString("group") != null) {
System.err.println("You cannot specify recipients by phone number and groups at the same time");
System.exit(2);
}
return ns;
}
private static ArgumentParser buildArgumentParser() {
ArgumentParser parser = ArgumentParsers.newFor("signal-cli")
.build()
.defaultHelp(true)
.description("Commandline interface for Signal.")
.version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION);
parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version());
parser.addArgument("--config")
.help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");
parser.addArgument("-n", "--busname")
.help("Name of the DBus.");
MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
mut.addArgument("-u", "--username").help("Specify your phone number, that will be used for verification.");
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());
ArgumentParser parser = buildBaseArgumentParser();
Subparsers subparsers = parser.addSubparsers()
.title("subcommands")
@ -312,6 +134,36 @@ public class Main {
Subparser subparser = subparsers.addParser(entry.getKey());
entry.getValue().attachToSubparser(subparser);
}
return parser;
}
private static ArgumentParser buildBaseArgumentParser() {
ArgumentParser parser = ArgumentParsers.newFor("signal-cli")
.build()
.defaultHelp(true)
.description("Commandline interface for Signal.")
.version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION);
parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version());
parser.addArgument("--verbose")
.help("Raise log level and include lib signal logs.")
.action(Arguments.storeTrue());
parser.addArgument("--config")
.help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");
parser.addArgument("-n", "--busname")
.help("Name of the DBus.");
MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
mut.addArgument("-u", "--username").help("Specify your phone number, that will be used for verification.");
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");
return parser;
}
}

View file

@ -42,6 +42,13 @@ public class Commands {
return commands;
}
public static Command getCommand(String commandKey) {
if (!commands.containsKey(commandKey)) {
return null;
}
return commands.get(commandKey);
}
private static void addCommand(String name, Command command) {
commands.put(name, command);
}

View file

@ -10,6 +10,8 @@ import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@ -20,6 +22,8 @@ import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
public class DaemonCommand implements LocalCommand {
private final static Logger logger = LoggerFactory.getLogger(ReceiveCommand.class);
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("--system")
@ -29,12 +33,19 @@ public class DaemonCommand implements LocalCommand {
.help("Dont 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 global \"--output=json\" option instead.\n\nOutput received messages in json format, one json object per line.")
.action(Arguments.storeTrue());
}
@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 the global \"--output=json\" instead.");
}
DBusConnection conn = null;
try {
try {
@ -66,7 +77,7 @@ public class DaemonCommand implements LocalCommand {
TimeUnit.HOURS,
false,
ignoreAttachments,
ns.getBoolean("json")
inJson
? new JsonDbusReceiveMessageHandler(m, conn, SIGNAL_OBJECTPATH)
: new DbusReceiveMessageHandler(m, conn, SIGNAL_OBJECTPATH));
return 0;

View file

@ -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
private 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 global \"--output=json\" 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") || ns.getBoolean("json");
// TODO delete later when "json" variable is removed
if (ns.getBoolean("json")) {
logger.warn("\"--json\" option has been deprecated, please use the global \"--output=json\" instead.");
}
// Get a map of registration statuses
Map<String, Boolean> registered;
try {
@ -42,7 +54,7 @@ public class GetUserStatusCommand implements LocalCommand {
}
// Output
if (ns.getBoolean("json")) {
if (inJson) {
List<JsonIsRegistered> objects = registered.entrySet()
.stream()
.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.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) {
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) {
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();
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<GroupInfo> 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<JsonGroup> 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<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

@ -18,6 +18,8 @@ import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.DateUtils;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.util.Base64;
import java.io.IOException;
@ -27,6 +29,9 @@ import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
// TODO delete later when "json" variable is removed
private final static Logger logger = LoggerFactory.getLogger(ReceiveCommand.class);
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-t", "--timeout")
@ -36,13 +41,21 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
.help("Dont 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 global \"--output=json\" 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 the global \"--output=json\" instead.");
}
if (inJson) {
jsonProcessor = new ObjectMapper();
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
@ -146,6 +159,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 the global \"--output=json\" instead.");
}
double timeout = 5;
if (ns.getDouble("timeout") != null) {
timeout = ns.getDouble("timeout");
@ -157,7 +177,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),

View file

@ -6,6 +6,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.File;
import java.io.IOException;
@ -14,7 +15,7 @@ public class UpdateProfileCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup().required(true);
final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup();
avatarOptions.addArgument("--avatar").help("Path to new profile avatar");
avatarOptions.addArgument("--remove-avatar").action(Arguments.storeTrue());
@ -30,7 +31,9 @@ public class UpdateProfileCommand implements LocalCommand {
boolean removeAvatar = ns.getBoolean("remove_avatar");
try {
File avatarFile = removeAvatar ? null : new File(avatarPath);
Optional<File> avatarFile = removeAvatar
? Optional.absent()
: avatarPath == null ? null : Optional.of(new File(avatarPath));
m.setProfile(name, avatarFile);
} catch (IOException e) {
System.err.println("UpdateAccount error: " + e.getMessage());

View file

@ -14,6 +14,7 @@ import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@ -193,7 +194,7 @@ public class DbusSignalImpl implements Signal {
}
final Pair<GroupId, List<SendMessageResult>> results = m.updateGroup(groupId == null
? null
: GroupId.unknownVersion(groupId), name, members, avatar);
: GroupId.unknownVersion(groupId), name, members, avatar == null ? null : new File(avatar));
checkSendMessageResults(0, results.second());
return results.first().serialize();
} catch (IOException e) {

View file

@ -19,6 +19,7 @@ class JsonDataMessage {
JsonQuote quote;
List<JsonMention> mentions;
List<JsonAttachment> attachments;
JsonSticker sticker;
JsonGroupInfo groupInfo;
JsonDataMessage(SignalServiceDataMessage dataMessage, Manager m) {
@ -60,15 +61,19 @@ class JsonDataMessage {
} else {
this.attachments = List.of();
}
if (dataMessage.getSticker().isPresent()) {
this.sticker = new JsonSticker(dataMessage.getSticker().get());
}
}
public JsonDataMessage(Signal.MessageReceived messageReceived) {
timestamp = messageReceived.getTimestamp();
message = messageReceived.getMessage();
groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
reaction = null; // TODO Replace these 3 with the proper commands
reaction = null; // TODO Replace these 4 with the proper commands
quote = null;
mentions = null;
sticker = null;
attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList());
}
@ -76,9 +81,10 @@ class JsonDataMessage {
timestamp = messageReceived.getTimestamp();
message = messageReceived.getMessage();
groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
reaction = null; // TODO Replace these 3 with the proper commands
reaction = null; // TODO Replace these 4 with the proper commands
quote = null;
mentions = null;
sticker = null;
attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList());
}
}

View file

@ -0,0 +1,18 @@
package org.asamk.signal.json;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.util.Base64;
public class JsonSticker {
String packId;
String packKey;
int stickerId;
public JsonSticker(SignalServiceDataMessage.Sticker sticker) {
this.packId = Base64.encodeBytes(sticker.getPackId());
this.packKey = Base64.encodeBytes(sticker.getPackKey());
this.stickerId = sticker.getStickerId();
// TODO also download sticker image ??
}
}

View file

@ -0,0 +1,55 @@
package org.asamk.signal.manager;
import org.asamk.signal.manager.util.IOUtils;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class AttachmentStore {
private final File attachmentsPath;
public AttachmentStore(final File attachmentsPath) {
this.attachmentsPath = attachmentsPath;
}
public void storeAttachmentPreview(
final SignalServiceAttachmentRemoteId attachmentId, final AttachmentStorer storer
) throws IOException {
storeAttachment(getAttachmentPreviewFile(attachmentId), storer);
}
public void storeAttachment(
final SignalServiceAttachmentRemoteId attachmentId, final AttachmentStorer storer
) throws IOException {
storeAttachment(getAttachmentFile(attachmentId), storer);
}
private void storeAttachment(final File attachmentFile, final AttachmentStorer storer) throws IOException {
createAttachmentsDir();
try (OutputStream output = new FileOutputStream(attachmentFile)) {
storer.store(output);
}
}
private File getAttachmentPreviewFile(SignalServiceAttachmentRemoteId attachmentId) {
return new File(attachmentsPath, attachmentId.toString() + ".preview");
}
public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
return new File(attachmentsPath, attachmentId.toString());
}
private void createAttachmentsDir() throws IOException {
IOUtils.createPrivateDirectories(attachmentsPath);
}
@FunctionalInterface
public interface AttachmentStorer {
void store(OutputStream outputStream) throws IOException;
}
}

View file

@ -0,0 +1,91 @@
package org.asamk.signal.manager;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.Utils;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
public class AvatarStore {
private final File avatarsPath;
public AvatarStore(final File avatarsPath) {
this.avatarsPath = avatarsPath;
}
public StreamDetails retrieveContactAvatar(SignalServiceAddress address) throws IOException {
return retrieveAvatar(getContactAvatarFile(address));
}
public StreamDetails retrieveProfileAvatar(SignalServiceAddress address) throws IOException {
return retrieveAvatar(getProfileAvatarFile(address));
}
public StreamDetails retrieveGroupAvatar(GroupId groupId) throws IOException {
final File groupAvatarFile = getGroupAvatarFile(groupId);
return retrieveAvatar(groupAvatarFile);
}
public void storeContactAvatar(SignalServiceAddress address, AvatarStorer storer) throws IOException {
storeAvatar(getContactAvatarFile(address), storer);
}
public void storeProfileAvatar(SignalServiceAddress address, AvatarStorer storer) throws IOException {
storeAvatar(getProfileAvatarFile(address), storer);
}
public void storeGroupAvatar(GroupId groupId, AvatarStorer storer) throws IOException {
storeAvatar(getGroupAvatarFile(groupId), storer);
}
public void deleteProfileAvatar(SignalServiceAddress address) throws IOException {
deleteAvatar(getProfileAvatarFile(address));
}
private StreamDetails retrieveAvatar(final File avatarFile) throws IOException {
if (!avatarFile.exists()) {
return null;
}
return Utils.createStreamDetailsFromFile(avatarFile);
}
private void storeAvatar(final File avatarFile, final AvatarStorer storer) throws IOException {
createAvatarsDir();
try (OutputStream output = new FileOutputStream(avatarFile)) {
storer.store(output);
}
}
private void deleteAvatar(final File avatarFile) throws IOException {
Files.delete(avatarFile.toPath());
}
private File getGroupAvatarFile(GroupId groupId) {
return new File(avatarsPath, "group-" + groupId.toBase64().replace("/", "_"));
}
private File getContactAvatarFile(SignalServiceAddress address) {
return new File(avatarsPath, "contact-" + address.getLegacyIdentifier());
}
private File getProfileAvatarFile(SignalServiceAddress address) {
return new File(avatarsPath, "profile-" + address.getLegacyIdentifier());
}
private void createAvatarsDir() throws IOException {
IOUtils.createPrivateDirectories(avatarsPath);
}
@FunctionalInterface
public interface AvatarStorer {
void store(OutputStream outputStream) throws IOException;
}
}

View file

@ -124,19 +124,19 @@ class SendGroupInfoRequestAction implements HandleAction {
}
}
class SendGroupUpdateAction implements HandleAction {
class SendGroupInfoAction implements HandleAction {
private final SignalServiceAddress address;
private final GroupIdV1 groupId;
public SendGroupUpdateAction(final SignalServiceAddress address, final GroupIdV1 groupId) {
public SendGroupInfoAction(final SignalServiceAddress address, final GroupIdV1 groupId) {
this.address = address;
this.groupId = groupId;
}
@Override
public void execute(Manager m) throws Throwable {
m.sendUpdateGroupMessage(groupId, address);
m.sendGroupInfoMessage(groupId, address);
}
@Override
@ -144,7 +144,7 @@ class SendGroupUpdateAction implements HandleAction {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SendGroupUpdateAction that = (SendGroupUpdateAction) o;
final SendGroupInfoAction that = (SendGroupInfoAction) o;
if (!address.equals(that.address)) return false;
return groupId.equals(that.groupId);

View file

@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
class JsonStickerPack {
public class JsonStickerPack {
@JsonProperty
public String title;

View file

@ -0,0 +1,41 @@
package org.asamk.signal.manager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.logging.SignalProtocolLogger;
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
public class LibSignalLogger implements SignalProtocolLogger {
private final static Logger logger = LoggerFactory.getLogger("LibSignal");
public static void initLogger() {
SignalProtocolLoggerProvider.setProvider(new LibSignalLogger());
}
private LibSignalLogger() {
}
@Override
public void log(final int priority, final String tag, final String message) {
final String logMessage = String.format("[%s]: %s", tag, message);
switch (priority) {
case SignalProtocolLogger.VERBOSE:
logger.trace(logMessage);
break;
case SignalProtocolLogger.DEBUG:
logger.debug(logMessage);
break;
case SignalProtocolLogger.INFO:
logger.info(logMessage);
break;
case SignalProtocolLogger.WARN:
logger.warn(logMessage);
break;
case SignalProtocolLogger.ERROR:
case SignalProtocolLogger.ASSERT:
logger.error(logMessage);
break;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -36,6 +36,7 @@ import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2Change
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
@ -47,7 +48,7 @@ import java.util.stream.Collectors;
public class GroupHelper {
final static Logger logger = LoggerFactory.getLogger(GroupHelper.class);
private final static Logger logger = LoggerFactory.getLogger(GroupHelper.class);
private final ProfileKeyCredentialProvider profileKeyCredentialProvider;
@ -99,7 +100,7 @@ public class GroupHelper {
}
public GroupInfoV2 createGroupV2(
String name, Collection<SignalServiceAddress> members, String avatarFile
String name, Collection<SignalServiceAddress> members, File avatarFile
) throws IOException {
final byte[] avatarBytes = readAvatarBytes(avatarFile);
final GroupsV2Operations.NewGroup newGroup = buildNewGroupV2(name, members, avatarBytes);
@ -132,7 +133,7 @@ public class GroupHelper {
return g;
}
private byte[] readAvatarBytes(final String avatarFile) throws IOException {
private byte[] readAvatarBytes(final File avatarFile) throws IOException {
final byte[] avatarBytes;
try (InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile)) {
avatarBytes = avatar == null ? null : IOUtils.readFully(avatar);
@ -194,7 +195,7 @@ public class GroupHelper {
}
public Pair<DecryptedGroup, GroupChange> updateGroupV2(
GroupInfoV2 groupInfoV2, String name, String avatarFile
GroupInfoV2 groupInfoV2, String name, File avatarFile
) throws IOException {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);

View file

@ -59,7 +59,7 @@ import java.util.stream.Collectors;
public class SignalAccount implements Closeable {
final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
private final ObjectMapper jsonProcessor = new ObjectMapper();
private final FileChannel fileChannel;
@ -138,6 +138,8 @@ public class SignalAccount implements Closeable {
account.registered = false;
account.migrateLegacyConfigs();
return account;
}
@ -179,6 +181,8 @@ public class SignalAccount implements Closeable {
account.registered = true;
account.isMultiDevice = true;
account.migrateLegacyConfigs();
return account;
}

View file

@ -37,7 +37,7 @@ import java.util.Map;
public class JsonGroupStore {
final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class);
private final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class);
private static final ObjectMapper jsonProcessor = new ObjectMapper();
public File groupCachePath;

View file

@ -11,7 +11,7 @@ import java.nio.file.Files;
public final class CachedMessage {
final static Logger logger = LoggerFactory.getLogger(CachedMessage.class);
private final static Logger logger = LoggerFactory.getLogger(CachedMessage.class);
private final File file;

View file

@ -18,7 +18,7 @@ import java.util.stream.Stream;
public class MessageCache {
final static Logger logger = LoggerFactory.getLogger(MessageCache.class);
private final static Logger logger = LoggerFactory.getLogger(MessageCache.class);
private final File messageCachePath;

View file

@ -5,8 +5,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import java.io.File;
public class SignalProfile {
@JsonProperty
@ -15,8 +13,6 @@ public class SignalProfile {
@JsonProperty
private final String name;
private final File avatarFile;
@JsonProperty
private final String unidentifiedAccess;
@ -29,14 +25,12 @@ public class SignalProfile {
public SignalProfile(
final String identityKey,
final String name,
final File avatarFile,
final String unidentifiedAccess,
final boolean unrestrictedUnidentifiedAccess,
final SignalServiceProfile.Capabilities capabilities
) {
this.identityKey = identityKey;
this.name = name;
this.avatarFile = avatarFile;
this.unidentifiedAccess = unidentifiedAccess;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
this.capabilities = new Capabilities();
@ -54,7 +48,6 @@ public class SignalProfile {
) {
this.identityKey = identityKey;
this.name = name;
this.avatarFile = null;
this.unidentifiedAccess = unidentifiedAccess;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
this.capabilities = capabilities;
@ -68,10 +61,6 @@ public class SignalProfile {
return name;
}
public File getAvatarFile() {
return avatarFile;
}
public String getUnidentifiedAccess() {
return unidentifiedAccess;
}
@ -94,7 +83,6 @@ public class SignalProfile {
+ name
+ '\''
+ ", avatarFile="
+ avatarFile
+ ", unidentifiedAccess='"
+ unidentifiedAccess
+ '\''

View file

@ -29,7 +29,7 @@ import java.util.UUID;
public class JsonIdentityKeyStore implements IdentityKeyStore {
final static Logger logger = LoggerFactory.getLogger(JsonIdentityKeyStore.class);
private final static Logger logger = LoggerFactory.getLogger(JsonIdentityKeyStore.class);
private final List<IdentityInfo> identities = new ArrayList<>();

View file

@ -21,7 +21,7 @@ import java.util.Map;
class JsonPreKeyStore implements PreKeyStore {
final static Logger logger = LoggerFactory.getLogger(JsonPreKeyStore.class);
private final static Logger logger = LoggerFactory.getLogger(JsonPreKeyStore.class);
private final Map<Integer, byte[]> store = new HashMap<>();

View file

@ -26,7 +26,7 @@ import java.util.UUID;
class JsonSessionStore implements SessionStore {
final static Logger logger = LoggerFactory.getLogger(JsonSessionStore.class);
private final static Logger logger = LoggerFactory.getLogger(JsonSessionStore.class);
private final List<SessionInfo> sessions = new ArrayList<>();

View file

@ -23,7 +23,7 @@ import java.util.Map;
class JsonSignedPreKeyStore implements SignedPreKeyStore {
final static Logger logger = LoggerFactory.getLogger(JsonSignedPreKeyStore.class);
private final static Logger logger = LoggerFactory.getLogger(JsonSignedPreKeyStore.class);
private final Map<Integer, byte[]> store = new HashMap<>();

View file

@ -4,15 +4,11 @@ import org.asamk.signal.manager.AttachmentInvalidException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
@ -34,19 +30,23 @@ public class AttachmentUtils {
}
public static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
InputStream attachmentStream = new FileInputStream(attachmentFile);
final long attachmentSize = attachmentFile.length();
final String mime = Utils.getFileMimeType(attachmentFile, "application/octet-stream");
final StreamDetails streamDetails = Utils.createStreamDetailsFromFile(attachmentFile);
return createAttachment(streamDetails, Optional.of(attachmentFile.getName()));
}
public static SignalServiceAttachmentStream createAttachment(
StreamDetails streamDetails, Optional<String> name
) {
// TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option
final long uploadTimestamp = System.currentTimeMillis();
Optional<byte[]> preview = Optional.absent();
Optional<String> caption = Optional.absent();
Optional<String> blurHash = Optional.absent();
final Optional<ResumableUploadSpec> resumableUploadSpec = Optional.absent();
return new SignalServiceAttachmentStream(attachmentStream,
mime,
attachmentSize,
Optional.of(attachmentFile.getName()),
return new SignalServiceAttachmentStream(streamDetails.getStream(),
streamDetails.getContentType(),
streamDetails.getLength(),
name,
false,
false,
preview,
@ -59,21 +59,4 @@ public class AttachmentUtils {
null,
resumableUploadSpec);
}
public static File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException {
InputStream input = stream.getInputStream();
try (OutputStream output = new FileOutputStream(outputFile)) {
byte[] buffer = new byte[4096];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
return outputFile;
}
}

View file

@ -1,10 +1,8 @@
package org.asamk.signal.manager.util;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -22,12 +20,14 @@ import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
public class IOUtils {
public static File createTempFile() throws IOException {
return File.createTempFile("signal_tmp_", ".tmp");
final File tempFile = File.createTempFile("signal-cli_tmp_", ".tmp");
tempFile.deleteOnExit();
return tempFile;
}
public static byte[] readFully(InputStream in) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Util.copy(in, baos);
IOUtils.copyStream(in, baos);
return baos.toByteArray();
}
@ -55,12 +55,17 @@ public class IOUtils {
}
}
public static void copyStreamToFile(InputStream input, File outputFile) throws IOException {
copyStreamToFile(input, outputFile, 8192);
public static void copyFileToStream(File inputFile, OutputStream output) throws IOException {
try (InputStream inputStream = new FileInputStream(inputFile)) {
copyStream(inputStream, output);
}
}
public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException {
try (OutputStream output = new FileOutputStream(outputFile)) {
public static void copyStream(InputStream input, OutputStream output) throws IOException {
copyStream(input, output, 4096);
}
public static void copyStream(InputStream input, OutputStream output, int bufferSize) throws IOException {
byte[] buffer = new byte[bufferSize];
int read;
@ -68,5 +73,4 @@ public class IOUtils {
output.write(buffer, 0, read);
}
}
}
}

View file

@ -5,12 +5,19 @@ import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECKeyPair;
import org.whispersystems.libsignal.ecc.ECPrivateKey;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Medium;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.util.Base64;
import java.util.ArrayList;
import java.util.List;
public class KeyUtils {
private KeyUtils() {
@ -24,6 +31,31 @@ public class KeyUtils {
return new IdentityKeyPair(djbIdentityKey, djbPrivateKey);
}
public static List<PreKeyRecord> generatePreKeyRecords(final int offset, final int batchSize) {
List<PreKeyRecord> records = new ArrayList<>(batchSize);
for (int i = 0; i < batchSize; i++) {
int preKeyId = (offset + i) % Medium.MAX_VALUE;
ECKeyPair keyPair = Curve.generateKeyPair();
PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
records.add(record);
}
return records;
}
public static SignedPreKeyRecord generateSignedPreKeyRecord(
final IdentityKeyPair identityKeyPair, final int signedPreKeyId
) {
ECKeyPair keyPair = Curve.generateKeyPair();
byte[] signature;
try {
signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
return new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
}
public static String createSignalingKey() {
return getSecret(52);
}

View file

@ -0,0 +1,45 @@
package org.asamk.signal.manager.util;
import org.asamk.signal.manager.storage.profiles.SignalProfile;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.util.Base64;
import java.io.IOException;
public class ProfileUtils {
public static SignalProfile decryptProfile(
final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
) {
ProfileCipher profileCipher = new ProfileCipher(profileKey);
try {
String name;
try {
name = encryptedProfile.getName() == null
? null
: new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName())));
} catch (IOException e) {
name = null;
}
String unidentifiedAccess;
try {
unidentifiedAccess = encryptedProfile.getUnidentifiedAccess() == null
|| !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess()))
? null
: encryptedProfile.getUnidentifiedAccess();
} catch (IOException e) {
unidentifiedAccess = null;
}
return new SignalProfile(encryptedProfile.getIdentityKey(),
name,
unidentifiedAccess,
encryptedProfile.isUnrestrictedUnidentifiedAccess(),
encryptedProfile.getCapabilities());
} catch (InvalidCiphertextException e) {
return null;
}
}
}

View file

@ -0,0 +1,113 @@
package org.asamk.signal.manager.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.asamk.signal.manager.JsonStickerPack;
import org.asamk.signal.manager.StickerPackInvalidException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class StickerUtils {
public static SignalServiceStickerManifestUpload getSignalServiceStickerManifestUpload(
final File file
) throws IOException, StickerPackInvalidException {
ZipFile zip = null;
String rootPath = null;
if (file.getName().endsWith(".zip")) {
zip = new ZipFile(file);
} else if (file.getName().equals("manifest.json")) {
rootPath = file.getParent();
} else {
throw new StickerPackInvalidException("Could not find manifest.json");
}
JsonStickerPack pack = parseStickerPack(rootPath, zip);
if (pack.stickers == null) {
throw new StickerPackInvalidException("Must set a 'stickers' field.");
}
if (pack.stickers.isEmpty()) {
throw new StickerPackInvalidException("Must include stickers.");
}
List<SignalServiceStickerManifestUpload.StickerInfo> stickers = new ArrayList<>(pack.stickers.size());
for (JsonStickerPack.JsonSticker sticker : pack.stickers) {
if (sticker.file == null) {
throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
}
Pair<InputStream, Long> data;
try {
data = getInputStreamAndLength(rootPath, zip, sticker.file);
} catch (IOException ignored) {
throw new StickerPackInvalidException("Could not find find " + sticker.file);
}
String contentType = Utils.getFileMimeType(new File(sticker.file), null);
SignalServiceStickerManifestUpload.StickerInfo stickerInfo = new SignalServiceStickerManifestUpload.StickerInfo(
data.first(),
data.second(),
Optional.fromNullable(sticker.emoji).or(""),
contentType);
stickers.add(stickerInfo);
}
SignalServiceStickerManifestUpload.StickerInfo cover = null;
if (pack.cover != null) {
if (pack.cover.file == null) {
throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
}
Pair<InputStream, Long> data;
try {
data = getInputStreamAndLength(rootPath, zip, pack.cover.file);
} catch (IOException ignored) {
throw new StickerPackInvalidException("Could not find find " + pack.cover.file);
}
String contentType = Utils.getFileMimeType(new File(pack.cover.file), null);
cover = new SignalServiceStickerManifestUpload.StickerInfo(data.first(),
data.second(),
Optional.fromNullable(pack.cover.emoji).or(""),
contentType);
}
return new SignalServiceStickerManifestUpload(pack.title, pack.author, cover, stickers);
}
private static JsonStickerPack parseStickerPack(String rootPath, ZipFile zip) throws IOException {
InputStream inputStream;
if (zip != null) {
inputStream = zip.getInputStream(zip.getEntry("manifest.json"));
} else {
inputStream = new FileInputStream((new File(rootPath, "manifest.json")));
}
return new ObjectMapper().readValue(inputStream, JsonStickerPack.class);
}
private static Pair<InputStream, Long> getInputStreamAndLength(
final String rootPath, final ZipFile zip, final String subfile
) throws IOException {
if (zip != null) {
final ZipEntry entry = zip.getEntry(subfile);
return new Pair<>(zip.getInputStream(entry), entry.getSize());
} else {
final File file = new File(rootPath, subfile);
return new Pair<>(new FileInputStream(file), file.length());
}
}
}

View file

@ -36,10 +36,7 @@ public class Utils {
public static StreamDetails createStreamDetailsFromFile(File file) throws IOException {
InputStream stream = new FileInputStream(file);
final long size = file.length();
String mime = Files.probeContentType(file.toPath());
if (mime == null) {
mime = "application/octet-stream";
}
final String mime = getFileMimeType(file, "application/octet-stream");
return new StreamDetails(stream, mime, size);
}