mirror of
https://github.com/AsamK/signal-cli
synced 2025-09-02 12:30:39 +00:00
Merge remote-tracking branch 'asamk/master'
This commit is contained in:
commit
52552c331d
36 changed files with 1212 additions and 796 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -10,3 +10,4 @@ local.properties
|
|||
.project
|
||||
.settings/
|
||||
out/
|
||||
.DS_Store
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- `--verbose` flag to increase log level
|
||||
|
||||
### Fixed
|
||||
- Disable registration lock before removing the PIN
|
||||
|
|
|
@ -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*::
|
||||
Don’t 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
|
||||
|
||||
|
|
206
src/main/java/org/asamk/signal/Cli.java
Normal file
206
src/main/java/org/asamk/signal/Cli.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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("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 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;
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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("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 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),
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
18
src/main/java/org/asamk/signal/json/JsonSticker.java
Normal file
18
src/main/java/org/asamk/signal/json/JsonSticker.java
Normal 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 ??
|
||||
}
|
||||
}
|
55
src/main/java/org/asamk/signal/manager/AttachmentStore.java
Normal file
55
src/main/java/org/asamk/signal/manager/AttachmentStore.java
Normal 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;
|
||||
}
|
||||
}
|
91
src/main/java/org/asamk/signal/manager/AvatarStore.java
Normal file
91
src/main/java/org/asamk/signal/manager/AvatarStore.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
class JsonStickerPack {
|
||||
public class JsonStickerPack {
|
||||
|
||||
@JsonProperty
|
||||
public String title;
|
||||
|
|
41
src/main/java/org/asamk/signal/manager/LibSignalLogger.java
Normal file
41
src/main/java/org/asamk/signal/manager/LibSignalLogger.java
Normal 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
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
+ '\''
|
||||
|
|
|
@ -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<>();
|
||||
|
||||
|
|
|
@ -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<>();
|
||||
|
||||
|
|
|
@ -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<>();
|
||||
|
||||
|
|
|
@ -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<>();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
113
src/main/java/org/asamk/signal/manager/util/StickerUtils.java
Normal file
113
src/main/java/org/asamk/signal/manager/util/StickerUtils.java
Normal 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue