diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 7a421966..16b180d1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -94,6 +94,8 @@ public interface Manager extends Closeable { void checkAccountState() throws IOException; + SignalAccount getAccount(); + Map> areUsersRegistered(Set numbers) throws IOException; void updateAccountAttributes(String deviceName) throws IOException; diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 0fd1eb33..80a0fea5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -247,6 +247,11 @@ public class ManagerImpl implements Manager { return account.getUsername(); } + @Override + public SignalAccount getAccount() { + return account; + } + @Override public void checkAccountState() throws IOException { if (account.getLastReceiveTimestamp() == 0) { diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index c42782f7..8f587872 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -116,8 +116,9 @@ public class RegistrationManager implements Closeable { 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 response; + final var captcha = captchaString == null ? null : captchaString.replace("signalcaptcha://", ""); if (voiceVerification) { response = accountManager.requestVoiceVerificationCode(getDefaultLocale(), Optional.fromNullable(captcha), diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index e7cd083f..8698428a 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -67,6 +67,14 @@ listAccounts() -> accountList:: Exceptions: None +listen(number) -> <>:: +* 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, voiceVerification) -> <>:: * number : Phone number * voiceVerification : true = use voice verification; false = use SMS verification @@ -78,6 +86,8 @@ registerWithCaptcha(number, voiceVerification, captcha) -> <>:: * voiceVerification : true = use voice verification; false = use SMS verification * captcha : Captcha string +Captcha strings may be obtained from `https://signalcaptchas.org/registration/generate.html` + Exceptions: Failure, InvalidNumber, RequiresCaptcha verify(number, verificationCode) -> <>:: diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 9829fe00..17e1b0b3 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -494,13 +494,16 @@ The path of the manifest.json or a zip file containing the sticker pack you wish === daemon 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 -objects under the same bus name. +If no `-u` username is given, zero or more local users as specified by the +`--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*:: Use DBus system bus instead of user bus. *--ignore-attachments*:: Don’t download attachments of received messages. +*--number* [NUMBER [NUMBER ...]]:: +List of zero or more numbers for anonymous daemon to listen to (default=all) == Examples diff --git a/src/main/java/org/asamk/SignalControl.java b/src/main/java/org/asamk/SignalControl.java index 911ccb61..fe06ecc2 100644 --- a/src/main/java/org/asamk/SignalControl.java +++ b/src/main/java/org/asamk/SignalControl.java @@ -28,6 +28,8 @@ public interface SignalControl extends DBusInterface { public String version(); + void listen(String number) throws Error.Failure; + List listAccounts(); interface Error { diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index 3d35ff8f..3c7b12bf 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -36,6 +36,7 @@ import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.File; import java.io.IOException; +import java.nio.channels.OverlappingFileLockException; import java.util.ArrayList; import java.util.List; @@ -125,12 +126,12 @@ public class App { return; } - final File dataPath; + final File settingsPath; var config = ns.getString("config"); if (config != null) { - dataPath = new File(config); + settingsPath = new File(config); } else { - dataPath = getDefaultDataPath(); + settingsPath = getDefaultSettingsPath(); } if (!ServiceConfig.getCapabilities().isGv2()) { @@ -157,22 +158,24 @@ public class App { 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 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; } if (username == null) { - var usernames = Manager.getAllLocalNumbers(dataPath); - - if (command instanceof MultiLocalCommand) { - handleMultiLocalCommand((MultiLocalCommand) command, - dataPath, - serviceEnvironment, - usernames, - outputWriter, - trustNewIdentity); - return; - } + var usernames = Manager.getAllLocalNumbers(settingsPath); if (usernames.size() == 0) { 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) { - handleRegistrationCommand((RegistrationCommand) command, username, dataPath, serviceEnvironment); + handleRegistrationCommand((RegistrationCommand) command, username, settingsPath, serviceEnvironment); return; } @@ -197,7 +200,7 @@ public class App { handleLocalCommand((LocalCommand) command, username, - dataPath, + settingsPath, serviceEnvironment, outputWriter, trustNewIdentity); @@ -205,23 +208,23 @@ public class App { private void handleProvisioningCommand( final ProvisioningCommand command, - final File dataPath, + final File settingsPath, final ServiceEnvironment serviceEnvironment, final OutputWriter outputWriter ) 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); } private void handleRegistrationCommand( final RegistrationCommand command, final String username, - final File dataPath, + final File settingsPath, final ServiceEnvironment serviceEnvironment ) throws CommandException { final RegistrationManager manager; try { - manager = RegistrationManager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT); + manager = RegistrationManager.init(username, settingsPath, serviceEnvironment, BaseConfig.USER_AGENT); } catch (Throwable e) { throw new UnexpectedErrorException("Error loading or creating state file: " + e.getMessage() @@ -231,6 +234,7 @@ public class App { } try (var m = manager) { command.handleCommand(ns, m); + m.close(); } catch (IOException e) { logger.warn("Cleanup failed", e); } @@ -239,13 +243,14 @@ public class App { private void handleLocalCommand( final LocalCommand command, final String username, - final File dataPath, + final File settingsPath, final ServiceEnvironment serviceEnvironment, final OutputWriter outputWriter, final TrustNewIdentity trustNewIdentity ) throws CommandException { - try (var m = loadManager(username, dataPath, serviceEnvironment, trustNewIdentity)) { + try (var m = loadManager(username, settingsPath, serviceEnvironment, trustNewIdentity)) { command.handleCommand(ns, m, outputWriter); + m.close(); } catch (IOException e) { logger.warn("Cleanup failed", e); } @@ -253,32 +258,35 @@ public class App { private void handleMultiLocalCommand( final MultiLocalCommand command, - final File dataPath, + final File settingsPath, final ServiceEnvironment serviceEnvironment, final List usernames, final OutputWriter outputWriter, final TrustNewIdentity trustNewIdentity ) throws CommandException { - final var managers = new ArrayList(); - for (String u : usernames) { - try { - managers.add(loadManager(u, dataPath, serviceEnvironment, trustNewIdentity)); - } catch (CommandException e) { - logger.warn("Ignoring {}: {}", u, e.getMessage()); + SignalCreator c = new SignalCreator() { + @Override + public File getSettingsPath() { + return settingsPath; } - } - command.handleCommand(ns, managers, new SignalCreator() { + @Override + public ServiceEnvironment getServiceEnvironment() { + return serviceEnvironment; + } @Override public ProvisioningManager getNewProvisioningManager() { - return ProvisioningManager.init(dataPath, serviceEnvironment, BaseConfig.USER_AGENT); + return ProvisioningManager.init(settingsPath, serviceEnvironment, BaseConfig.USER_AGENT); } @Override 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(); + command.handleCommand(ns, managers, c, outputWriter, trustNewIdentity); for (var m : managers) { 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 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 TrustNewIdentity trustNewIdentity ) throws CommandException { Manager manager; try { - manager = Manager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT, trustNewIdentity); + manager = Manager.init(username, settingsPath, serviceEnvironment, BaseConfig.USER_AGENT, trustNewIdentity); } catch (NotRegisteredException e) { throw new UserErrorException("User " + username + " is not registered."); + } catch (OverlappingFileLockException e) { + throw new UserErrorException("User " + username + " is already listening."); } catch (Throwable e) { throw new UnexpectedErrorException("Error loading state file for user " + username @@ -313,6 +371,13 @@ public class App { try { manager.checkAccountState(); } 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); } @@ -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"); } } diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 02063b87..ac48b610 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -4,6 +4,7 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.App; import org.asamk.signal.DbusConfig; import org.asamk.signal.DbusReceiveMessageHandler; import org.asamk.signal.JsonDbusReceiveMessageHandler; @@ -13,14 +14,19 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; 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.DbusSignalImpl; 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.exceptions.DBusException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; import java.io.IOException; import java.util.List; import java.util.concurrent.TimeUnit; @@ -28,6 +34,9 @@ import java.util.concurrent.TimeUnit; public class DaemonCommand implements MultiLocalCommand { private final static Logger logger = LoggerFactory.getLogger(DaemonCommand.class); + public static DBusConnection.DBusBusType dBusType; + public static TrustNewIdentity trustNewIdentity; + public static OutputWriter outputWriter; @Override public String getName() { @@ -43,6 +52,8 @@ public class DaemonCommand implements MultiLocalCommand { subparser.addArgument("--ignore-attachments") .help("Don’t download attachments of received messages.") .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 @@ -54,6 +65,12 @@ public class DaemonCommand implements MultiLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) 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")); DBusConnection.DBusBusType busType; @@ -63,6 +80,12 @@ public class DaemonCommand implements MultiLocalCommand { busType = DBusConnection.DBusBusType.SESSION; } + this.dBusType = busType; + this.trustNewIdentity = trustNewIdentity; + this.outputWriter = outputWriter; + + checkMacOS(); + try (var conn = DBusConnection.getConnection(busType)) { var objectPath = DbusConfig.getObjectPath(); var t = run(conn, objectPath, m, outputWriter, ignoreAttachments); @@ -73,7 +96,10 @@ public class DaemonCommand implements MultiLocalCommand { t.join(); } 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); throw new UnexpectedErrorException("Dbus command failed", e); } @@ -81,8 +107,9 @@ public class DaemonCommand implements MultiLocalCommand { @Override public void handleCommand( - final Namespace ns, final List managers, final SignalCreator c, final OutputWriter outputWriter + final Namespace ns, final List managers, final SignalCreator c, final OutputWriter outputWriter, TrustNewIdentity trustNewIdentity ) throws CommandException { + //anonymous mode boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); DBusConnection.DBusBusType busType; @@ -92,6 +119,12 @@ public class DaemonCommand implements MultiLocalCommand { busType = DBusConnection.DBusBusType.SESSION; } + this.dBusType = busType; + this.trustNewIdentity = trustNewIdentity; + this.outputWriter = outputWriter; + + checkMacOS(); + try (var conn = DBusConnection.getConnection(busType)) { final var signalControl = new DbusSignalControlImpl(c, m -> { try { @@ -104,19 +137,59 @@ public class DaemonCommand implements MultiLocalCommand { }, DbusConfig.getObjectPath()); conn.exportObject(signalControl); + List daemonUsernames = ns.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) { signalControl.addManager(m); } conn.requestBusName(DbusConfig.getBusname()); + logger.info("Starting daemon."); 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); 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( DBusConnection conn, String objectPath, Manager m, OutputWriter outputWriter, boolean ignoreAttachments ) throws DBusException { diff --git a/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java b/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java index 1c01a6ae..a573e803 100644 --- a/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java +++ b/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java @@ -5,19 +5,12 @@ import net.sourceforge.argparse4j.inf.Namespace; import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import java.util.List; public interface MultiLocalCommand extends LocalCommand { + void handleCommand(Namespace ns, List 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 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); - } } diff --git a/src/main/java/org/asamk/signal/commands/SignalCreator.java b/src/main/java/org/asamk/signal/commands/SignalCreator.java index 675d7f2a..038850b6 100644 --- a/src/main/java/org/asamk/signal/commands/SignalCreator.java +++ b/src/main/java/org/asamk/signal/commands/SignalCreator.java @@ -2,11 +2,17 @@ package org.asamk.signal.commands; import org.asamk.signal.manager.ProvisioningManager; import org.asamk.signal.manager.RegistrationManager; +import org.asamk.signal.manager.config.ServiceEnvironment; +import java.io.File; import java.io.IOException; public interface SignalCreator { + File getSettingsPath(); + + ServiceEnvironment getServiceEnvironment(); + ProvisioningManager getNewProvisioningManager(); RegistrationManager getNewRegistrationManager(String username) throws IOException; diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 3124a5b0..36724c0a 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -23,6 +23,7 @@ import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; 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.Profile; import org.asamk.signal.manager.storage.recipients.RecipientAddress; @@ -80,6 +81,11 @@ public class DbusManagerImpl implements Manager { throw new UnsupportedOperationException(); } + @Override + public SignalAccount getAccount() { + throw new UnsupportedOperationException(); + } + @Override public Map> areUsersRegistered(final Set numbers) throws IOException { final var numbersList = new ArrayList<>(numbers); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java index be628bde..385e456c 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java @@ -1,36 +1,65 @@ package org.asamk.signal.dbus; import org.asamk.SignalControl; +import org.asamk.SignalControl.Error; +import org.asamk.signal.App; import org.asamk.signal.BaseConfig; 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.exceptions.CommandException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.ProvisioningManager; import org.asamk.signal.manager.RegistrationManager; 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.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.signalservice.api.KeyBackupServicePinException; import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import java.io.BufferedReader; +import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.net.URI; +import java.nio.channels.OverlappingFileLockException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; public class DbusSignalControlImpl implements org.asamk.SignalControl { - private final SignalCreator c; - private final Function newManagerRunner; + private static SignalCreator c; + private static Function newManagerRunner; - private final List> receiveThreads = new ArrayList<>(); - private final Object stopTrigger = new Object(); - private final String objectPath; + private static List> receiveThreads = new ArrayList<>(); + private static Object stopTrigger = new Object(); + 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( final SignalCreator c, final Function newManagerRunner, final String objectPath @@ -38,9 +67,10 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl { this.c = c; this.newManagerRunner = newManagerRunner; this.objectPath = objectPath; + this.busType = busType; } - public void addManager(Manager m) { + public static void addManager(Manager m) { var thread = newManagerRunner.apply(m); if (thread == null) { return; @@ -125,7 +155,9 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl { ) throws Error.Failure, Error.InvalidNumber { try (final RegistrationManager registrationManager = c.getNewRegistrationManager(number)) { final Manager manager = registrationManager.verifyAccount(verificationCode, pin); + logger.info("Registration of " + number + " verified"); addManager(manager); + registrationManager.close(); } catch (IOException | KeyBackupSystemNoDataException | KeyBackupServicePinException e) { throw new SignalControl.Error.Failure(e.getClass().getSimpleName() + " " + e.getMessage()); } @@ -139,9 +171,19 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl { new Thread(() -> { try { final Manager manager = provisioningManager.finishDeviceLink(newDeviceName); + logger.info("Linking of " + newDeviceName + " successful"); addManager(manager); - } catch (IOException | TimeoutException | UserAlreadyExists e) { - e.printStackTrace(); + //no need to close provisioningManager; it cleaned up during finishDeviceLink + } 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(); return deviceLinkUri.toString(); @@ -155,6 +197,51 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl { return BaseConfig.PROJECT_VERSION; } + @Override + public void listen(String number) { + try { + File settingsPath = c.getSettingsPath(); + List 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 public List listAccounts() { synchronized (receiveThreads) {