DBus listen(number<s>) and daemon \-\-number

implement listen method for DBus
listen(number<s>) -> <>::

add \-\-number option to daemon subcommand
- permits starting daemon in anonymous mode with zero or more numbers
- numbers may be added to daemon with listen() and removed with unlisten()

change misleading "dataPath" to `settingsPath` in App.java
- settingsPath=~/.local/share/signal-cli, while dataPath=~/.local/share/signal-cli/data

only use FileLock when necessary in App.java, and unlock when appropriate

update documentation
This commit is contained in:
John Freed 2021-10-05 13:17:30 +02:00
parent 26594dd0ee
commit 690636b83d
12 changed files with 317 additions and 64 deletions

View file

@ -94,6 +94,8 @@ public interface Manager extends Closeable {
void checkAccountState() throws IOException; void checkAccountState() throws IOException;
SignalAccount getAccount();
Map<String, Pair<String, UUID>> areUsersRegistered(Set<String> numbers) throws IOException; Map<String, Pair<String, UUID>> areUsersRegistered(Set<String> numbers) throws IOException;
void updateAccountAttributes(String deviceName) throws IOException; void updateAccountAttributes(String deviceName) throws IOException;

View file

@ -247,6 +247,11 @@ public class ManagerImpl implements Manager {
return account.getUsername(); return account.getUsername();
} }
@Override
public SignalAccount getAccount() {
return account;
}
@Override @Override
public void checkAccountState() throws IOException { public void checkAccountState() throws IOException {
if (account.getLastReceiveTimestamp() == 0) { if (account.getLastReceiveTimestamp() == 0) {

View file

@ -116,8 +116,9 @@ public class RegistrationManager implements Closeable {
return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent); return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
} }
public void register(boolean voiceVerification, String captcha) throws IOException { public void register(boolean voiceVerification, String captchaString) throws IOException {
final ServiceResponse<RequestVerificationCodeResponse> response; final ServiceResponse<RequestVerificationCodeResponse> response;
final var captcha = captchaString == null ? null : captchaString.replace("signalcaptcha://", "");
if (voiceVerification) { if (voiceVerification) {
response = accountManager.requestVoiceVerificationCode(getDefaultLocale(), response = accountManager.requestVoiceVerificationCode(getDefaultLocale(),
Optional.fromNullable(captcha), Optional.fromNullable(captcha),

View file

@ -67,6 +67,14 @@ listAccounts() -> accountList<as>::
Exceptions: None Exceptions: None
listen(number<s>) -> <>::
* number : Phone number
Starting checking the Signal servers on behalf of this number, and export a DBus object path for it.
Fails if user is not already registered.
Exceptions: Failure
register(number<s>, voiceVerification<b>) -> <>:: register(number<s>, voiceVerification<b>) -> <>::
* number : Phone number * number : Phone number
* voiceVerification : true = use voice verification; false = use SMS verification * voiceVerification : true = use voice verification; false = use SMS verification
@ -78,6 +86,8 @@ registerWithCaptcha(number<s>, voiceVerification<b>, captcha<s>) -> <>::
* voiceVerification : true = use voice verification; false = use SMS verification * voiceVerification : true = use voice verification; false = use SMS verification
* captcha : Captcha string * captcha : Captcha string
Captcha strings may be obtained from `https://signalcaptchas.org/registration/generate.html`
Exceptions: Failure, InvalidNumber, RequiresCaptcha Exceptions: Failure, InvalidNumber, RequiresCaptcha
verify(number<s>, verificationCode<s>) -> <>:: verify(number<s>, verificationCode<s>) -> <>::

View file

@ -494,13 +494,16 @@ The path of the manifest.json or a zip file containing the sticker pack you wish
=== daemon === daemon
signal-cli can run in daemon mode and provides an experimental dbus interface. signal-cli can run in daemon mode and provides an experimental dbus interface.
If no `-u` username is given, all local users will be exported as separate dbus If no `-u` username is given, zero or more local users as specified by the
objects under the same bus name. `--number` option will be exported as separate dbus objects under the same bus name.
If `--number` is omitted, all local users will be exported.
*--system*:: *--system*::
Use DBus system bus instead of user bus. Use DBus system bus instead of user bus.
*--ignore-attachments*:: *--ignore-attachments*::
Dont download attachments of received messages. Dont download attachments of received messages.
*--number* [NUMBER [NUMBER ...]]::
List of zero or more numbers for anonymous daemon to listen to (default=all)
== Examples == Examples

View file

@ -28,6 +28,8 @@ public interface SignalControl extends DBusInterface {
public String version(); public String version();
void listen(String number) throws Error.Failure;
List<DBusPath> listAccounts(); List<DBusPath> listAccounts();
interface Error { interface Error {

View file

@ -36,6 +36,7 @@ import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.channels.OverlappingFileLockException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -125,12 +126,12 @@ public class App {
return; return;
} }
final File dataPath; final File settingsPath;
var config = ns.getString("config"); var config = ns.getString("config");
if (config != null) { if (config != null) {
dataPath = new File(config); settingsPath = new File(config);
} else { } else {
dataPath = getDefaultDataPath(); settingsPath = getDefaultSettingsPath();
} }
if (!ServiceConfig.getCapabilities().isGv2()) { if (!ServiceConfig.getCapabilities().isGv2()) {
@ -157,22 +158,24 @@ public class App {
throw new UserErrorException("You cannot specify a username (phone number) when linking"); throw new UserErrorException("You cannot specify a username (phone number) when linking");
} }
handleProvisioningCommand((ProvisioningCommand) command, dataPath, serviceEnvironment, outputWriter); handleProvisioningCommand((ProvisioningCommand) command, settingsPath, serviceEnvironment, outputWriter);
return;
}
if (command instanceof MultiLocalCommand) {
List<String> usernames = new ArrayList<>();
if (username == null) {
//anonymous mode
handleMultiLocalCommand((MultiLocalCommand) command, settingsPath, serviceEnvironment, usernames, outputWriter, trustNewIdentity);
} else {
//single-user mode
handleMultiLocalCommand((MultiLocalCommand) command, settingsPath, serviceEnvironment, username, outputWriter, trustNewIdentity);
}
return; return;
} }
if (username == null) { if (username == null) {
var usernames = Manager.getAllLocalNumbers(dataPath); var usernames = Manager.getAllLocalNumbers(settingsPath);
if (command instanceof MultiLocalCommand) {
handleMultiLocalCommand((MultiLocalCommand) command,
dataPath,
serviceEnvironment,
usernames,
outputWriter,
trustNewIdentity);
return;
}
if (usernames.size() == 0) { if (usernames.size() == 0) {
throw new UserErrorException("No local users found, you first need to register or link an account"); throw new UserErrorException("No local users found, you first need to register or link an account");
@ -187,7 +190,7 @@ public class App {
} }
if (command instanceof RegistrationCommand) { if (command instanceof RegistrationCommand) {
handleRegistrationCommand((RegistrationCommand) command, username, dataPath, serviceEnvironment); handleRegistrationCommand((RegistrationCommand) command, username, settingsPath, serviceEnvironment);
return; return;
} }
@ -197,7 +200,7 @@ public class App {
handleLocalCommand((LocalCommand) command, handleLocalCommand((LocalCommand) command,
username, username,
dataPath, settingsPath,
serviceEnvironment, serviceEnvironment,
outputWriter, outputWriter,
trustNewIdentity); trustNewIdentity);
@ -205,23 +208,23 @@ public class App {
private void handleProvisioningCommand( private void handleProvisioningCommand(
final ProvisioningCommand command, final ProvisioningCommand command,
final File dataPath, final File settingsPath,
final ServiceEnvironment serviceEnvironment, final ServiceEnvironment serviceEnvironment,
final OutputWriter outputWriter final OutputWriter outputWriter
) throws CommandException { ) throws CommandException {
var pm = ProvisioningManager.init(dataPath, serviceEnvironment, BaseConfig.USER_AGENT); var pm = ProvisioningManager.init(settingsPath, serviceEnvironment, BaseConfig.USER_AGENT);
command.handleCommand(ns, pm, outputWriter); command.handleCommand(ns, pm, outputWriter);
} }
private void handleRegistrationCommand( private void handleRegistrationCommand(
final RegistrationCommand command, final RegistrationCommand command,
final String username, final String username,
final File dataPath, final File settingsPath,
final ServiceEnvironment serviceEnvironment final ServiceEnvironment serviceEnvironment
) throws CommandException { ) throws CommandException {
final RegistrationManager manager; final RegistrationManager manager;
try { try {
manager = RegistrationManager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT); manager = RegistrationManager.init(username, settingsPath, serviceEnvironment, BaseConfig.USER_AGENT);
} catch (Throwable e) { } catch (Throwable e) {
throw new UnexpectedErrorException("Error loading or creating state file: " throw new UnexpectedErrorException("Error loading or creating state file: "
+ e.getMessage() + e.getMessage()
@ -231,6 +234,7 @@ public class App {
} }
try (var m = manager) { try (var m = manager) {
command.handleCommand(ns, m); command.handleCommand(ns, m);
m.close();
} catch (IOException e) { } catch (IOException e) {
logger.warn("Cleanup failed", e); logger.warn("Cleanup failed", e);
} }
@ -239,13 +243,14 @@ public class App {
private void handleLocalCommand( private void handleLocalCommand(
final LocalCommand command, final LocalCommand command,
final String username, final String username,
final File dataPath, final File settingsPath,
final ServiceEnvironment serviceEnvironment, final ServiceEnvironment serviceEnvironment,
final OutputWriter outputWriter, final OutputWriter outputWriter,
final TrustNewIdentity trustNewIdentity final TrustNewIdentity trustNewIdentity
) throws CommandException { ) throws CommandException {
try (var m = loadManager(username, dataPath, serviceEnvironment, trustNewIdentity)) { try (var m = loadManager(username, settingsPath, serviceEnvironment, trustNewIdentity)) {
command.handleCommand(ns, m, outputWriter); command.handleCommand(ns, m, outputWriter);
m.close();
} catch (IOException e) { } catch (IOException e) {
logger.warn("Cleanup failed", e); logger.warn("Cleanup failed", e);
} }
@ -253,32 +258,35 @@ public class App {
private void handleMultiLocalCommand( private void handleMultiLocalCommand(
final MultiLocalCommand command, final MultiLocalCommand command,
final File dataPath, final File settingsPath,
final ServiceEnvironment serviceEnvironment, final ServiceEnvironment serviceEnvironment,
final List<String> usernames, final List<String> usernames,
final OutputWriter outputWriter, final OutputWriter outputWriter,
final TrustNewIdentity trustNewIdentity final TrustNewIdentity trustNewIdentity
) throws CommandException { ) throws CommandException {
final var managers = new ArrayList<Manager>(); SignalCreator c = new SignalCreator() {
for (String u : usernames) { @Override
try { public File getSettingsPath() {
managers.add(loadManager(u, dataPath, serviceEnvironment, trustNewIdentity)); return settingsPath;
} catch (CommandException e) {
logger.warn("Ignoring {}: {}", u, e.getMessage());
} }
}
command.handleCommand(ns, managers, new SignalCreator() { @Override
public ServiceEnvironment getServiceEnvironment() {
return serviceEnvironment;
}
@Override @Override
public ProvisioningManager getNewProvisioningManager() { public ProvisioningManager getNewProvisioningManager() {
return ProvisioningManager.init(dataPath, serviceEnvironment, BaseConfig.USER_AGENT); return ProvisioningManager.init(settingsPath, serviceEnvironment, BaseConfig.USER_AGENT);
} }
@Override @Override
public RegistrationManager getNewRegistrationManager(String username) throws IOException { public RegistrationManager getNewRegistrationManager(String username) throws IOException {
return RegistrationManager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT); return RegistrationManager.init(username, settingsPath, serviceEnvironment, BaseConfig.USER_AGENT);
} }
}, outputWriter); };
final var managers = new ArrayList<Manager>();
command.handleCommand(ns, managers, c, outputWriter, trustNewIdentity);
for (var m : managers) { for (var m : managers) {
try { try {
@ -289,17 +297,67 @@ public class App {
} }
} }
private Manager loadManager( private void handleMultiLocalCommand(
final MultiLocalCommand command,
final File settingsPath,
final ServiceEnvironment serviceEnvironment,
final String username, final String username,
final File dataPath, final OutputWriter outputWriter,
final TrustNewIdentity trustNewIdentity
) throws CommandException {
SignalCreator c = new SignalCreator() {
@Override
public File getSettingsPath() {
return settingsPath;
}
@Override
public ServiceEnvironment getServiceEnvironment() {
return serviceEnvironment;
}
@Override
public ProvisioningManager getNewProvisioningManager() {
return ProvisioningManager.init(settingsPath, serviceEnvironment, BaseConfig.USER_AGENT);
}
@Override
public RegistrationManager getNewRegistrationManager(String username) throws IOException {
return RegistrationManager.init(username, settingsPath, serviceEnvironment, BaseConfig.USER_AGENT);
}
};
Manager manager = null;
try {
manager = loadManager(username, settingsPath, serviceEnvironment, trustNewIdentity);
} catch (CommandException e) {
logger.warn("Ignoring {}: {}", username, e.getMessage());
}
command.handleCommand(ns, manager, c, outputWriter, trustNewIdentity);
try {
manager.close();
} catch (IOException e) {
logger.warn("Cleanup failed", e);
}
}
public static Manager loadManager(
final String username,
final File settingsPath,
final ServiceEnvironment serviceEnvironment, final ServiceEnvironment serviceEnvironment,
final TrustNewIdentity trustNewIdentity final TrustNewIdentity trustNewIdentity
) throws CommandException { ) throws CommandException {
Manager manager; Manager manager;
try { try {
manager = Manager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT, trustNewIdentity); manager = Manager.init(username, settingsPath, serviceEnvironment, BaseConfig.USER_AGENT, trustNewIdentity);
} catch (NotRegisteredException e) { } catch (NotRegisteredException e) {
throw new UserErrorException("User " + username + " is not registered."); throw new UserErrorException("User " + username + " is not registered.");
} catch (OverlappingFileLockException e) {
throw new UserErrorException("User " + username + " is already listening.");
} catch (Throwable e) { } catch (Throwable e) {
throw new UnexpectedErrorException("Error loading state file for user " throw new UnexpectedErrorException("Error loading state file for user "
+ username + username
@ -313,6 +371,13 @@ public class App {
try { try {
manager.checkAccountState(); manager.checkAccountState();
} catch (IOException e) { } catch (IOException e) {
/* In case account isn't registered on Signal servers, close it locally,
* thus removing the FileLock so another daemon can get it.
*/
try {
manager.getAccount().close();
} catch (IOException ignore) {
}
throw new IOErrorException("Error while checking account " + username + ": " + e.getMessage(), e); throw new IOErrorException("Error while checking account " + username + ": " + e.getMessage(), e);
} }
@ -361,9 +426,9 @@ public class App {
} }
/** /**
* @return the default data directory to be used by signal-cli. * @return the default settings directory to be used by signal-cli.
*/ */
private static File getDefaultDataPath() { private static File getDefaultSettingsPath() {
return new File(IOUtils.getDataHomeDir(), "signal-cli"); return new File(IOUtils.getDataHomeDir(), "signal-cli");
} }
} }

View file

@ -4,6 +4,7 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.App;
import org.asamk.signal.DbusConfig; import org.asamk.signal.DbusConfig;
import org.asamk.signal.DbusReceiveMessageHandler; import org.asamk.signal.DbusReceiveMessageHandler;
import org.asamk.signal.JsonDbusReceiveMessageHandler; import org.asamk.signal.JsonDbusReceiveMessageHandler;
@ -13,14 +14,19 @@ import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter; import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.dbus.DbusSignalControlImpl; import org.asamk.signal.dbus.DbusSignalControlImpl;
import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.config.ServiceEnvironment;
import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -28,6 +34,9 @@ import java.util.concurrent.TimeUnit;
public class DaemonCommand implements MultiLocalCommand { public class DaemonCommand implements MultiLocalCommand {
private final static Logger logger = LoggerFactory.getLogger(DaemonCommand.class); private final static Logger logger = LoggerFactory.getLogger(DaemonCommand.class);
public static DBusConnection.DBusBusType dBusType;
public static TrustNewIdentity trustNewIdentity;
public static OutputWriter outputWriter;
@Override @Override
public String getName() { public String getName() {
@ -43,6 +52,8 @@ public class DaemonCommand implements MultiLocalCommand {
subparser.addArgument("--ignore-attachments") subparser.addArgument("--ignore-attachments")
.help("Dont download attachments of received messages.") .help("Dont download attachments of received messages.")
.action(Arguments.storeTrue()); .action(Arguments.storeTrue());
subparser.addArgument("--number", "--numbers").help("Phone numbers").nargs("*")
.help("List of zero or more numbers for anonymous daemon to listen to (default=all)");
} }
@Override @Override
@ -54,6 +65,12 @@ public class DaemonCommand implements MultiLocalCommand {
public void handleCommand( public void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException { ) throws CommandException {
handleCommand(ns, m, null, outputWriter, null);
}
@Override
public void handleCommand(final Namespace ns, final Manager m, final SignalCreator c, final OutputWriter outputWriter, final TrustNewIdentity trustNewIdentity) throws CommandException {
//single-user mode
boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
DBusConnection.DBusBusType busType; DBusConnection.DBusBusType busType;
@ -63,6 +80,12 @@ public class DaemonCommand implements MultiLocalCommand {
busType = DBusConnection.DBusBusType.SESSION; busType = DBusConnection.DBusBusType.SESSION;
} }
this.dBusType = busType;
this.trustNewIdentity = trustNewIdentity;
this.outputWriter = outputWriter;
checkMacOS();
try (var conn = DBusConnection.getConnection(busType)) { try (var conn = DBusConnection.getConnection(busType)) {
var objectPath = DbusConfig.getObjectPath(); var objectPath = DbusConfig.getObjectPath();
var t = run(conn, objectPath, m, outputWriter, ignoreAttachments); var t = run(conn, objectPath, m, outputWriter, ignoreAttachments);
@ -73,7 +96,10 @@ public class DaemonCommand implements MultiLocalCommand {
t.join(); t.join();
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {
} }
} catch (DBusException | IOException e) { } catch (DBusException e) {
logger.error("Dbus command failed", e);
throw new UserErrorException("Dbus command failed, daemon already started on this bus.");
} catch (IOException e) {
logger.error("Dbus command failed", e); logger.error("Dbus command failed", e);
throw new UnexpectedErrorException("Dbus command failed", e); throw new UnexpectedErrorException("Dbus command failed", e);
} }
@ -81,8 +107,9 @@ public class DaemonCommand implements MultiLocalCommand {
@Override @Override
public void handleCommand( public void handleCommand(
final Namespace ns, final List<Manager> managers, final SignalCreator c, final OutputWriter outputWriter final Namespace ns, final List<Manager> managers, final SignalCreator c, final OutputWriter outputWriter, TrustNewIdentity trustNewIdentity
) throws CommandException { ) throws CommandException {
//anonymous mode
boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
DBusConnection.DBusBusType busType; DBusConnection.DBusBusType busType;
@ -92,6 +119,12 @@ public class DaemonCommand implements MultiLocalCommand {
busType = DBusConnection.DBusBusType.SESSION; busType = DBusConnection.DBusBusType.SESSION;
} }
this.dBusType = busType;
this.trustNewIdentity = trustNewIdentity;
this.outputWriter = outputWriter;
checkMacOS();
try (var conn = DBusConnection.getConnection(busType)) { try (var conn = DBusConnection.getConnection(busType)) {
final var signalControl = new DbusSignalControlImpl(c, m -> { final var signalControl = new DbusSignalControlImpl(c, m -> {
try { try {
@ -104,19 +137,59 @@ public class DaemonCommand implements MultiLocalCommand {
}, DbusConfig.getObjectPath()); }, DbusConfig.getObjectPath());
conn.exportObject(signalControl); conn.exportObject(signalControl);
List<String> daemonUsernames = ns.<String>getList("number");
File settingsPath = c.getSettingsPath();
ServiceEnvironment serviceEnvironment = c.getServiceEnvironment();
if (daemonUsernames == null) {
//--numbers option was not given, so add all local usernames
daemonUsernames = Manager.getAllLocalNumbers(settingsPath);
if (daemonUsernames.size() == 0) {
logger.error("No users are registered yet.");
throw new UserErrorException("No users are registered yetTry again with signal-cli daemon --numbers");
}
}
for (String u : daemonUsernames) {
try {
managers.add(App.loadManager(u, settingsPath, serviceEnvironment, trustNewIdentity));
} catch (CommandException e) {
logger.warn("Ignoring {}: {}", u, e.getMessage());
}
}
for (var m : managers) { for (var m : managers) {
signalControl.addManager(m); signalControl.addManager(m);
} }
conn.requestBusName(DbusConfig.getBusname()); conn.requestBusName(DbusConfig.getBusname());
logger.info("Starting daemon.");
signalControl.run(); signalControl.run();
} catch (DBusException | IOException e) { } catch (DBusException e) {
logger.error("Dbus command failed", e);
throw new UserErrorException("Dbus command failed, daemon alreadytarted on this bus.");
} catch (IOException e ) {
logger.error("Dbus command failed", e); logger.error("Dbus command failed", e);
throw new UnexpectedErrorException("Dbus command failed", e); throw new UnexpectedErrorException("Dbus command failed", e);
} }
} }
private void checkMacOS() throws UserErrorException {
if (System.getProperty("os.name").toLowerCase().startsWith("mac ")) {
String dBusVar = System.getenv("DBUS_LAUNCHD_SESSION_BUS_SOCKET");
if (dBusVar == null || dBusVar.isBlank()) {
String message = "\n\n" +
"*************************************" +
"\n\nDBUS_LAUNCHD_SESSION_BUS_SOCKET is not set. Issue the command:\n\n" +
"export DBUS_LAUNCHD_SESSION_BUS_SOCKET=$(launchctl getenv DBUS_LAUNCHD_SESSION_BUS_SOCKET)\n" +
"\nand then try again.\n\n" +
"*************************************";
throw new UserErrorException(message);
}
}
}
private Thread run( private Thread run(
DBusConnection conn, String objectPath, Manager m, OutputWriter outputWriter, boolean ignoreAttachments DBusConnection conn, String objectPath, Manager m, OutputWriter outputWriter, boolean ignoreAttachments
) throws DBusException { ) throws DBusException {

View file

@ -5,19 +5,12 @@ import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.signal.OutputWriter; import org.asamk.signal.OutputWriter;
import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import java.util.List; import java.util.List;
public interface MultiLocalCommand extends LocalCommand { public interface MultiLocalCommand extends LocalCommand {
void handleCommand(Namespace ns, List<Manager> m, SignalCreator c, OutputWriter outputWriter, TrustNewIdentity trustNewIdentity) throws CommandException;
void handleCommand(Namespace ns, Manager m, SignalCreator c, OutputWriter outputWriter, TrustNewIdentity trustNewIdentity) throws CommandException;
void handleCommand(
Namespace ns, List<Manager> m, SignalCreator c, OutputWriter outputWriter
) throws CommandException;
@Override
default void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
handleCommand(ns, List.of(m), null, outputWriter);
}
} }

View file

@ -2,11 +2,17 @@ package org.asamk.signal.commands;
import org.asamk.signal.manager.ProvisioningManager; import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.RegistrationManager; import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.config.ServiceEnvironment;
import java.io.File;
import java.io.IOException; import java.io.IOException;
public interface SignalCreator { public interface SignalCreator {
File getSettingsPath();
ServiceEnvironment getServiceEnvironment();
ProvisioningManager getNewProvisioningManager(); ProvisioningManager getNewProvisioningManager();
RegistrationManager getNewRegistrationManager(String username) throws IOException; RegistrationManager getNewRegistrationManager(String username) throws IOException;

View file

@ -23,6 +23,7 @@ import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Contact;
import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientAddress;
@ -80,6 +81,11 @@ public class DbusManagerImpl implements Manager {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public SignalAccount getAccount() {
throw new UnsupportedOperationException();
}
@Override @Override
public Map<String, Pair<String, UUID>> areUsersRegistered(final Set<String> numbers) throws IOException { public Map<String, Pair<String, UUID>> areUsersRegistered(final Set<String> numbers) throws IOException {
final var numbersList = new ArrayList<>(numbers); final var numbersList = new ArrayList<>(numbers);

View file

@ -1,36 +1,65 @@
package org.asamk.signal.dbus; package org.asamk.signal.dbus;
import org.asamk.SignalControl; import org.asamk.SignalControl;
import org.asamk.SignalControl.Error;
import org.asamk.signal.App;
import org.asamk.signal.BaseConfig; import org.asamk.signal.BaseConfig;
import org.asamk.signal.DbusConfig; import org.asamk.signal.DbusConfig;
import org.asamk.signal.DbusReceiveMessageHandler;
import org.asamk.signal.JsonDbusReceiveMessageHandler;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.commands.DaemonCommand;
import org.asamk.signal.commands.SignalCreator; import org.asamk.signal.commands.SignalCreator;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.ProvisioningManager; import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.RegistrationManager; import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.UserAlreadyExists; import org.asamk.signal.manager.UserAlreadyExists;
import org.asamk.signal.manager.config.ServiceEnvironment;
import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.DBusPath;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.KeyBackupServicePinException; import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException; import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI; import java.net.URI;
import java.nio.channels.OverlappingFileLockException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class DbusSignalControlImpl implements org.asamk.SignalControl { public class DbusSignalControlImpl implements org.asamk.SignalControl {
private final SignalCreator c; private static SignalCreator c;
private final Function<Manager, Thread> newManagerRunner; private static Function<Manager, Thread> newManagerRunner;
private final List<Pair<Manager, Thread>> receiveThreads = new ArrayList<>(); private static List<Pair<Manager, Thread>> receiveThreads = new ArrayList<>();
private final Object stopTrigger = new Object(); private static Object stopTrigger = new Object();
private final String objectPath; private static String objectPath;
private static DBusConnection.DBusBusType busType;
public static RegistrationManager registrationManager;
public static ProvisioningManager provisioningManager;
private final static Logger logger = LoggerFactory.getLogger(DbusSignalControlImpl.class);
public DbusSignalControlImpl( public DbusSignalControlImpl(
final SignalCreator c, final Function<Manager, Thread> newManagerRunner, final String objectPath final SignalCreator c, final Function<Manager, Thread> newManagerRunner, final String objectPath
@ -38,9 +67,10 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl {
this.c = c; this.c = c;
this.newManagerRunner = newManagerRunner; this.newManagerRunner = newManagerRunner;
this.objectPath = objectPath; this.objectPath = objectPath;
this.busType = busType;
} }
public void addManager(Manager m) { public static void addManager(Manager m) {
var thread = newManagerRunner.apply(m); var thread = newManagerRunner.apply(m);
if (thread == null) { if (thread == null) {
return; return;
@ -125,7 +155,9 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl {
) throws Error.Failure, Error.InvalidNumber { ) throws Error.Failure, Error.InvalidNumber {
try (final RegistrationManager registrationManager = c.getNewRegistrationManager(number)) { try (final RegistrationManager registrationManager = c.getNewRegistrationManager(number)) {
final Manager manager = registrationManager.verifyAccount(verificationCode, pin); final Manager manager = registrationManager.verifyAccount(verificationCode, pin);
logger.info("Registration of " + number + " verified");
addManager(manager); addManager(manager);
registrationManager.close();
} catch (IOException | KeyBackupSystemNoDataException | KeyBackupServicePinException e) { } catch (IOException | KeyBackupSystemNoDataException | KeyBackupServicePinException e) {
throw new SignalControl.Error.Failure(e.getClass().getSimpleName() + " " + e.getMessage()); throw new SignalControl.Error.Failure(e.getClass().getSimpleName() + " " + e.getMessage());
} }
@ -139,9 +171,19 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl {
new Thread(() -> { new Thread(() -> {
try { try {
final Manager manager = provisioningManager.finishDeviceLink(newDeviceName); final Manager manager = provisioningManager.finishDeviceLink(newDeviceName);
logger.info("Linking of " + newDeviceName + " successful");
addManager(manager); addManager(manager);
} catch (IOException | TimeoutException | UserAlreadyExists e) { //no need to close provisioningManager; it cleaned up during finishDeviceLink
e.printStackTrace(); } catch (TimeoutException e) {
throw new SignalControl.Error.Failure(e.getClass().getSimpleName() + ": Link request timed out, please try again.");
} catch (IOException e) {
throw new SignalControl.Error.Failure(e.getClass().getSimpleName() + ": Link request error: " + e.getMessage());
} catch (UserAlreadyExists e) {
throw new SignalControl.Error.Failure(e.getClass().getSimpleName() + ": The user "
+ e.getNumber()
+ " already exists\nDelete \""
+ e.getFileName()
+ "\" before trying again.");
} }
}).start(); }).start();
return deviceLinkUri.toString(); return deviceLinkUri.toString();
@ -155,6 +197,51 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl {
return BaseConfig.PROJECT_VERSION; return BaseConfig.PROJECT_VERSION;
} }
@Override
public void listen(String number) {
try {
File settingsPath = c.getSettingsPath();
List<String> usernames = Manager.getAllLocalNumbers(settingsPath);
if (!usernames.contains(number)) {
throw new Error.Failure("Listen: " + number + " is not registered.");
}
String objectPath = DbusConfig.getObjectPath(number);
DBusConnection.DBusBusType busType = DaemonCommand.dBusType;
ServiceEnvironment serviceEnvironment = c.getServiceEnvironment();
TrustNewIdentity trustNewIdentity = DaemonCommand.trustNewIdentity;
//create new manager for this number
final Manager m = App.loadManager(number, settingsPath, serviceEnvironment, trustNewIdentity);
addManager(m);
final var thread = new Thread(() -> {
try {
OutputWriter outputWriter = DaemonCommand.outputWriter;
boolean ignoreAttachments = false;
DBusConnection conn = DBusConnection.getConnection(busType);
while (!Thread.interrupted()) {
try {
final var receiveMessageHandler = outputWriter instanceof JsonWriter
? new JsonDbusReceiveMessageHandler(m, (JsonWriter) outputWriter, conn, objectPath)
: new DbusReceiveMessageHandler(m, (PlainTextWriter) outputWriter, conn, objectPath);
m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, receiveMessageHandler);
break;
} catch (IOException e) {
logger.warn("Receiving messages failed, retrying", e);
}
}
} catch (DBusException e) {
throw new Error.Failure(e.getClass().getSimpleName() + " Listen error: " + e.getMessage());
}
});
} catch (OverlappingFileLockException e) {
logger.warn("Ignoring {}: {}", number, e.getMessage());
throw new Error.Failure(e.getClass().getSimpleName() + " Already listening: " + e.getMessage());
} catch (CommandException e) {
logger.warn("Ignoring {}: {}", number, e.getMessage());
throw new Error.Failure(e.getClass().getSimpleName() + " Listen error: " + e.getMessage());
}
}
@Override @Override
public List<DBusPath> listAccounts() { public List<DBusPath> listAccounts() {
synchronized (receiveThreads) { synchronized (receiveThreads) {