var hasCaughtUpWithOldMessages = false;
- while (true) {
+ while (!Thread.interrupted()) {
SignalServiceEnvelope envelope;
SignalServiceContent content = null;
Exception exception = null;
--- /dev/null
+package org.asamk;
+
+import org.freedesktop.dbus.DBusPath;
+import org.freedesktop.dbus.exceptions.DBusExecutionException;
+import org.freedesktop.dbus.interfaces.DBusInterface;
+
+import java.util.List;
+
+/**
+ * DBus interface for the org.asamk.SignalControl interface.
+ * Including emitted Signals and returned Errors.
+ */
+public interface SignalControl extends DBusInterface {
+
+ void register(
+ String number, boolean voiceVerification
+ ) throws Error.Failure, Error.InvalidNumber, Error.RequiresCaptcha;
+
+ void registerWithCaptcha(
+ String number, boolean voiceVerification, String captcha
+ ) throws Error.Failure, Error.InvalidNumber, Error.RequiresCaptcha;
+
+ void verify(String number, String verificationCode) throws Error.Failure, Error.InvalidNumber;
+
+ void verifyWithPin(String number, String verificationCode, String pin) throws Error.Failure, Error.InvalidNumber;
+
+ String link(String newDeviceName) throws Error.Failure;
+
+ public String version();
+
+ List<DBusPath> listAccounts();
+
+ interface Error {
+
+ class Failure extends DBusExecutionException {
+
+ public Failure(final String message) {
+ super(message);
+ }
+ }
+
+ class InvalidNumber extends DBusExecutionException {
+
+ public InvalidNumber(final String message) {
+ super(message);
+ }
+ }
+
+ class RequiresCaptcha extends DBusExecutionException {
+
+ public RequiresCaptcha(final String message) {
+ super(message);
+ }
+ }
+ }
+}
import org.asamk.signal.commands.MultiLocalCommand;
import org.asamk.signal.commands.ProvisioningCommand;
import org.asamk.signal.commands.RegistrationCommand;
+import org.asamk.signal.commands.SignalCreator;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
}
}
- command.handleCommand(ns, managers);
+ command.handleCommand(ns, managers, new SignalCreator() {
+ @Override
+ public ProvisioningManager getNewProvisioningManager() {
+ return ProvisioningManager.init(dataPath, serviceEnvironment, BaseConfig.USER_AGENT);
+ }
+
+ @Override
+ public RegistrationManager getNewRegistrationManager(String username) throws IOException {
+ return RegistrationManager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT);
+ }
+ });
for (var m : managers) {
try {
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
+import org.asamk.signal.dbus.DbusSignalControlImpl;
import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.slf4j.LoggerFactory;
import java.io.IOException;
-import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
}
@Override
- public void handleCommand(final Namespace ns, final List<Manager> managers) throws CommandException {
+ public void handleCommand(
+ final Namespace ns, final List<Manager> managers, SignalCreator c
+ ) throws CommandException {
boolean ignoreAttachments = ns.getBoolean("ignore-attachments");
DBusConnection.DBusBusType busType;
}
try (var conn = DBusConnection.getConnection(busType)) {
- var receiveThreads = new ArrayList<Thread>();
+ final var signalControl = new DbusSignalControlImpl(c, m -> {
+ try {
+ final var objectPath = DbusConfig.getObjectPath(m.getUsername());
+ return run(conn, objectPath, m, ignoreAttachments);
+ } catch (DBusException e) {
+ logger.error("Failed to export object", e);
+ return null;
+ }
+ }, DbusConfig.getObjectPath());
+ conn.exportObject(signalControl);
+
for (var m : managers) {
- var objectPath = DbusConfig.getObjectPath(m.getUsername());
- var thread = run(conn, objectPath, m, ignoreAttachments);
- receiveThreads.add(thread);
+ signalControl.addManager(m);
}
conn.requestBusName(DbusConfig.getBusname());
- for (var t : receiveThreads) {
- try {
- t.join();
- } catch (InterruptedException ignored) {
- }
- }
+ signalControl.run();
} catch (DBusException | IOException e) {
logger.error("Dbus command failed", e);
throw new UnexpectedErrorException("Dbus command failed");
private Thread run(
DBusConnection conn, String objectPath, Manager m, boolean ignoreAttachments
) throws DBusException {
- conn.exportObject(objectPath, new DbusSignalImpl(m));
+ conn.exportObject(new DbusSignalImpl(m, objectPath));
+
+ logger.info("Exported dbus object: " + objectPath);
final var thread = new Thread(() -> {
while (true) {
}
});
- logger.info("Exported dbus object: " + objectPath);
-
thread.start();
return thread;
void handleCommand(Namespace ns, Signal signal) throws CommandException;
default void handleCommand(final Namespace ns, final Manager m) throws CommandException {
- handleCommand(ns, new DbusSignalImpl(m));
+ handleCommand(ns, new DbusSignalImpl(m, null));
}
}
public interface MultiLocalCommand extends LocalCommand {
- void handleCommand(Namespace ns, List<Manager> m) throws CommandException;
+ void handleCommand(Namespace ns, List<Manager> m, final SignalCreator c) throws CommandException;
@Override
default void handleCommand(final Namespace ns, final Manager m) throws CommandException {
- handleCommand(ns, List.of(m));
+ handleCommand(ns, List.of(m), null);
}
}
--- /dev/null
+package org.asamk.signal.commands;
+
+import org.asamk.signal.manager.ProvisioningManager;
+import org.asamk.signal.manager.RegistrationManager;
+
+import java.io.IOException;
+
+public interface SignalCreator {
+
+ ProvisioningManager getNewProvisioningManager();
+
+ RegistrationManager getNewRegistrationManager(String username) throws IOException;
+}
--- /dev/null
+package org.asamk.signal.dbus;
+
+import org.asamk.SignalControl;
+import org.asamk.signal.BaseConfig;
+import org.asamk.signal.DbusConfig;
+import org.asamk.signal.commands.SignalCreator;
+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.freedesktop.dbus.DBusPath;
+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 java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class DbusSignalControlImpl implements org.asamk.SignalControl {
+
+ private final SignalCreator c;
+ private final Function<Manager, Thread> newManagerRunner;
+
+ private final List<Pair<Manager, Thread>> receiveThreads = new ArrayList<>();
+ private final Object stopTrigger = new Object();
+ private final String objectPath;
+
+ public DbusSignalControlImpl(
+ final SignalCreator c, final Function<Manager, Thread> newManagerRunner, final String objectPath
+ ) {
+ this.c = c;
+ this.newManagerRunner = newManagerRunner;
+ this.objectPath = objectPath;
+ }
+
+ public void addManager(Manager m) {
+ var thread = newManagerRunner.apply(m);
+ if (thread == null) {
+ return;
+ }
+ synchronized (receiveThreads) {
+ receiveThreads.add(new Pair<>(m, thread));
+ }
+ }
+
+ public void run() {
+ synchronized (stopTrigger) {
+ try {
+ stopTrigger.wait();
+ } catch (InterruptedException ignored) {
+ }
+ }
+
+ synchronized (receiveThreads) {
+ for (var t : receiveThreads) {
+ t.second().interrupt();
+ }
+ }
+ while (true) {
+ final Thread thread;
+ synchronized (receiveThreads) {
+ if (receiveThreads.size() == 0) {
+ break;
+ }
+ var pair = receiveThreads.remove(0);
+ thread = pair.second();
+ }
+ try {
+ thread.join();
+ } catch (InterruptedException ignored) {
+ }
+ }
+ }
+
+ @Override
+ public boolean isRemote() {
+ return false;
+ }
+
+ @Override
+ public String getObjectPath() {
+ return objectPath;
+ }
+
+ @Override
+ public void register(
+ final String number, final boolean voiceVerification
+ ) throws Error.Failure, Error.InvalidNumber {
+ registerWithCaptcha(number, voiceVerification, null);
+ }
+
+ @Override
+ public void registerWithCaptcha(
+ final String number, final boolean voiceVerification, final String captcha
+ ) throws Error.Failure, Error.InvalidNumber {
+ try (final RegistrationManager registrationManager = c.getNewRegistrationManager(number)) {
+ registrationManager.register(voiceVerification, captcha);
+ } catch (CaptchaRequiredException e) {
+ String message = captcha == null ? "Captcha required for verification." : "Invalid captcha given.";
+ throw new SignalControl.Error.RequiresCaptcha(message);
+ } catch (IOException e) {
+ throw new SignalControl.Error.Failure(e.getClass().getSimpleName() + " " + e.getMessage());
+ }
+ }
+
+ @Override
+ public void verify(final String number, final String verificationCode) throws Error.Failure, Error.InvalidNumber {
+ verifyWithPin(number, verificationCode, null);
+ }
+
+ @Override
+ public void verifyWithPin(
+ final String number, final String verificationCode, final String pin
+ ) throws Error.Failure, Error.InvalidNumber {
+ try (final RegistrationManager registrationManager = c.getNewRegistrationManager(number)) {
+ final Manager manager = registrationManager.verifyAccount(verificationCode, pin);
+ addManager(manager);
+ } catch (IOException | KeyBackupSystemNoDataException | KeyBackupServicePinException e) {
+ throw new SignalControl.Error.Failure(e.getClass().getSimpleName() + " " + e.getMessage());
+ }
+ }
+
+ @Override
+ public String link(final String newDeviceName) throws Error.Failure {
+ try {
+ final ProvisioningManager provisioningManager = c.getNewProvisioningManager();
+ final URI deviceLinkUri = provisioningManager.getDeviceLinkUri();
+ new Thread(() -> {
+ try {
+ final Manager manager = provisioningManager.finishDeviceLink(newDeviceName);
+ addManager(manager);
+ } catch (IOException | TimeoutException | UserAlreadyExists e) {
+ e.printStackTrace();
+ }
+ }).start();
+ return deviceLinkUri.toString();
+ } catch (TimeoutException | IOException e) {
+ throw new SignalControl.Error.Failure(e.getClass().getSimpleName() + " " + e.getMessage());
+ }
+ }
+
+ @Override
+ public String version() {
+ return BaseConfig.PROJECT_VERSION;
+ }
+
+ @Override
+ public List<DBusPath> listAccounts() {
+ synchronized (receiveThreads) {
+ return receiveThreads.stream()
+ .map(Pair::first)
+ .map(Manager::getUsername)
+ .map(u -> new DBusPath(DbusConfig.getObjectPath(u)))
+ .collect(Collectors.toList());
+ }
+ }
+}
public class DbusSignalImpl implements Signal {
private final Manager m;
+ private final String objectPath;
- public DbusSignalImpl(final Manager m) {
+ public DbusSignalImpl(final Manager m, final String objectPath) {
this.m = m;
+ this.objectPath = objectPath;
}
@Override
@Override
public String getObjectPath() {
- return null;
+ return objectPath;
}
@Override