@Override
public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException {
+ captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
+
dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
}
-package org.asamk.signal.commands;
-
-import org.asamk.signal.manager.Manager;
-import org.asamk.signal.manager.ProvisioningManager;
-import org.asamk.signal.manager.RegistrationManager;
+package org.asamk.signal.manager;
import java.io.IOException;
import java.util.List;
import java.util.function.Consumer;
-public interface SignalCreator {
+public interface MultiAccountManager extends AutoCloseable {
List<String> getAccountNumbers();
- void addManager(Manager m);
-
void addOnManagerAddedHandler(Consumer<Manager> handler);
+ void addOnManagerRemovedHandler(Consumer<Manager> handler);
+
Manager getManager(String phoneNumber);
ProvisioningManager getNewProvisioningManager();
RegistrationManager getNewRegistrationManager(String username) throws IOException;
+
+ @Override
+ void close();
}
--- /dev/null
+package org.asamk.signal.manager;
+
+import org.asamk.signal.manager.config.ServiceEnvironment;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+public class MultiAccountManagerImpl implements MultiAccountManager {
+
+ private final static Logger logger = LoggerFactory.getLogger(MultiAccountManagerImpl.class);
+
+ private final Set<Consumer<Manager>> onManagerAddedHandlers = new HashSet<>();
+ private final Set<Consumer<Manager>> onManagerRemovedHandlers = new HashSet<>();
+ private final Set<Manager> managers = new HashSet<>();
+ private final File dataPath;
+ private final ServiceEnvironment serviceEnvironment;
+ private final String userAgent;
+
+ public MultiAccountManagerImpl(
+ final Collection<Manager> managers,
+ final File dataPath,
+ final ServiceEnvironment serviceEnvironment,
+ final String userAgent
+ ) {
+ this.managers.addAll(managers);
+ this.dataPath = dataPath;
+ this.serviceEnvironment = serviceEnvironment;
+ this.userAgent = userAgent;
+ }
+
+ @Override
+ public List<String> getAccountNumbers() {
+ synchronized (managers) {
+ return managers.stream().map(Manager::getSelfNumber).collect(Collectors.toList());
+ }
+ }
+
+ void addManager(final Manager m) {
+ synchronized (managers) {
+ if (managers.contains(m)) {
+ return;
+ }
+ managers.add(m);
+ }
+ synchronized (onManagerAddedHandlers) {
+ for (final var handler : onManagerAddedHandlers) {
+ handler.accept(m);
+ }
+ }
+ }
+
+ @Override
+ public void addOnManagerAddedHandler(final Consumer<Manager> handler) {
+ synchronized (onManagerAddedHandlers) {
+ onManagerAddedHandlers.add(handler);
+ }
+ }
+
+ @Override
+ public void addOnManagerRemovedHandler(final Consumer<Manager> handler) {
+ synchronized (onManagerRemovedHandlers) {
+ onManagerRemovedHandlers.add(handler);
+ }
+ }
+
+ @Override
+ public Manager getManager(final String account) {
+ synchronized (managers) {
+ return managers.stream().filter(m -> m.getSelfNumber().equals(account)).findFirst().orElse(null);
+ }
+ }
+
+ @Override
+ public ProvisioningManager getNewProvisioningManager() {
+ return ProvisioningManager.init(dataPath, serviceEnvironment, userAgent, this::addManager);
+ }
+
+ @Override
+ public RegistrationManager getNewRegistrationManager(String account) throws IOException {
+ return RegistrationManager.init(account, dataPath, serviceEnvironment, userAgent, this::addManager);
+ }
+
+ @Override
+ public void close() {
+ synchronized (managers) {
+ for (var m : managers) {
+ try {
+ m.close();
+ } catch (IOException e) {
+ logger.warn("Cleanup failed", e);
+ }
+ }
+ managers.clear();
+ }
+ }
+}
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
public class ProvisioningManager {
private final PathConfig pathConfig;
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
private final String userAgent;
+ private final Consumer<Manager> newManagerListener;
private final SignalServiceAccountManager accountManager;
private final IdentityKeyPair tempIdentityKey;
private final int registrationId;
private final String password;
- ProvisioningManager(PathConfig pathConfig, ServiceEnvironmentConfig serviceEnvironmentConfig, String userAgent) {
+ ProvisioningManager(
+ PathConfig pathConfig,
+ ServiceEnvironmentConfig serviceEnvironmentConfig,
+ String userAgent,
+ final Consumer<Manager> newManagerListener
+ ) {
this.pathConfig = pathConfig;
this.serviceEnvironmentConfig = serviceEnvironmentConfig;
this.userAgent = userAgent;
+ this.newManagerListener = newManagerListener;
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
registrationId = KeyHelper.generateRegistrationId(false);
public static ProvisioningManager init(
File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent
+ ) {
+ return init(settingsPath, serviceEnvironment, userAgent, null);
+ }
+
+ public static ProvisioningManager init(
+ File settingsPath,
+ ServiceEnvironment serviceEnvironment,
+ String userAgent,
+ Consumer<Manager> newManagerListener
) {
var pathConfig = PathConfig.createDefault(settingsPath);
final var serviceConfiguration = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
- return new ProvisioningManager(pathConfig, serviceConfiguration, userAgent);
+ return new ProvisioningManager(pathConfig, serviceConfiguration, userAgent, newManagerListener);
}
public URI getDeviceLinkUri() throws TimeoutException, IOException {
return new DeviceLinkInfo(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
}
- public Manager finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExists {
+ public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExists {
var ret = accountManager.getNewDeviceRegistration(tempIdentityKey);
var number = ret.getNumber();
"Failed to request sync messages from linked device, data can be requested again with `sendSyncRequest`.");
}
- final var result = m;
- account = null;
- m = null;
-
- return result;
+ if (newManagerListener != null) {
+ newManagerListener.accept(m);
+ m = null;
+ }
+ return number;
} finally {
if (m != null) {
m.close();
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
+import java.util.function.Consumer;
public class RegistrationManager implements Closeable {
private final PathConfig pathConfig;
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
private final String userAgent;
+ private final Consumer<Manager> newManagerListener;
private final SignalServiceAccountManager accountManager;
private final PinHelper pinHelper;
SignalAccount account,
PathConfig pathConfig,
ServiceEnvironmentConfig serviceEnvironmentConfig,
- String userAgent
+ String userAgent,
+ Consumer<Manager> newManagerListener
) {
this.account = account;
this.pathConfig = pathConfig;
this.serviceEnvironmentConfig = serviceEnvironmentConfig;
this.userAgent = userAgent;
+ this.newManagerListener = newManagerListener;
GroupsV2Operations groupsV2Operations;
try {
public static RegistrationManager init(
String number, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent
+ ) throws IOException {
+ return init(number, settingsPath, serviceEnvironment, userAgent, null);
+ }
+
+ public static RegistrationManager init(
+ String number,
+ File settingsPath,
+ ServiceEnvironment serviceEnvironment,
+ String userAgent,
+ Consumer<Manager> newManagerListener
) throws IOException {
var pathConfig = PathConfig.createDefault(settingsPath);
profileKey,
TrustNewIdentity.ON_FIRST_USE);
- return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
+ return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent, newManagerListener);
}
var account = SignalAccount.load(pathConfig.dataPath(), number, true, TrustNewIdentity.ON_FIRST_USE);
- return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
+ return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent, newManagerListener);
}
public void register(boolean voiceVerification, String captcha) throws IOException, CaptchaRequiredException {
+ captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
final ServiceResponse<RequestVerificationCodeResponse> response;
if (voiceVerification) {
response = accountManager.requestVoiceVerificationCode(Utils.getDefaultLocale(),
}
}
- public Manager verifyAccount(
+ public void verifyAccount(
String verificationCode, String pin
) throws IOException, PinLockedException, IncorrectPinException {
verificationCode = verificationCode.replace("-", "");
logger.warn("Failed to set default profile: {}", e.getMessage());
}
- final var result = m;
- m = null;
-
- return result;
+ if (newManagerListener != null) {
+ newManagerListener.accept(m);
+ m = null;
+ }
} finally {
if (m != null) {
m.close();
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.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.dbus.DbusManagerImpl;
import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.MultiAccountManagerImpl;
import org.asamk.signal.manager.NotRegisteredException;
import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.RegistrationManager;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
import static net.sourceforge.argparse4j.DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS;
throw new UserErrorException("No local users found, you first need to register or link an account");
} else if (accounts.size() > 1) {
throw new UserErrorException(
- "Multiple users found, you need to specify a account (phone number) with -u");
+ "Multiple users found, you need to specify an account (phone number) with -a");
}
account = accounts.get(0);
+ e.getClass().getSimpleName()
+ ")", e);
}
- try (var m = manager) {
- command.handleCommand(ns, m);
+ try (manager) {
+ command.handleCommand(ns, manager);
} catch (IOException e) {
logger.warn("Cleanup failed", e);
}
final TrustNewIdentity trustNewIdentity
) throws CommandException {
final var managers = new ArrayList<Manager>();
- try {
- for (String u : accounts) {
- try {
- managers.add(loadManager(u, dataPath, serviceEnvironment, trustNewIdentity));
- } catch (CommandException e) {
- logger.warn("Ignoring {}: {}", u, e.getMessage());
- }
+ for (String a : accounts) {
+ try {
+ managers.add(loadManager(a, dataPath, serviceEnvironment, trustNewIdentity));
+ } catch (CommandException e) {
+ logger.warn("Ignoring {}: {}", a, e.getMessage());
}
+ }
- command.handleCommand(ns, new SignalCreator() {
- private final List<Consumer<Manager>> onManagerAddedHandlers = new ArrayList<>();
-
- @Override
- public List<String> getAccountNumbers() {
- synchronized (managers) {
- return managers.stream().map(Manager::getSelfNumber).collect(Collectors.toList());
- }
- }
-
- @Override
- public void addManager(final Manager m) {
- synchronized (managers) {
- if (!managers.contains(m)) {
- managers.add(m);
- for (final var handler : onManagerAddedHandlers) {
- handler.accept(m);
- }
- }
- }
- }
-
- @Override
- public void addOnManagerAddedHandler(final Consumer<Manager> handler) {
- onManagerAddedHandlers.add(handler);
- }
-
- @Override
- public Manager getManager(final String phoneNumber) {
- synchronized (managers) {
- return managers.stream()
- .filter(m -> m.getSelfNumber().equals(phoneNumber))
- .findFirst()
- .orElse(null);
- }
- }
-
- @Override
- public ProvisioningManager getNewProvisioningManager() {
- return ProvisioningManager.init(dataPath, serviceEnvironment, BaseConfig.USER_AGENT);
- }
-
- @Override
- public RegistrationManager getNewRegistrationManager(String account) throws IOException {
- return RegistrationManager.init(account, dataPath, serviceEnvironment, BaseConfig.USER_AGENT);
- }
- }, outputWriter);
- } finally {
- synchronized (managers) {
- for (var m : managers) {
- try {
- m.close();
- } catch (IOException e) {
- logger.warn("Cleanup failed", e);
- }
- }
- managers.clear();
- }
+ try (var multiAccountManager = new MultiAccountManagerImpl(managers,
+ dataPath,
+ serviceEnvironment,
+ BaseConfig.USER_AGENT)) {
+ command.handleCommand(ns, multiAccountManager, outputWriter);
}
}
import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.jsonrpc.SignalJsonRpcDispatcherHandler;
import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.MultiAccountManager;
import org.asamk.signal.util.IOUtils;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
@Override
public void handleCommand(
- final Namespace ns, final SignalCreator c, final OutputWriter outputWriter
+ final Namespace ns, final MultiAccountManager c, final OutputWriter outputWriter
) throws CommandException {
logger.info("Starting daemon in multi-account mode");
final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
}
private void runSocketMultiAccount(
- final SignalCreator c, final ServerSocketChannel serverChannel, final boolean noReceiveOnStart
+ final MultiAccountManager c, final ServerSocketChannel serverChannel, final boolean noReceiveOnStart
) {
runSocket(serverChannel, channel -> {
final var handler = getSignalJsonRpcDispatcherHandler(channel, noReceiveOnStart);
}
private void runDbusMultiAccount(
- final SignalCreator c, final boolean noReceiveOnStart, final boolean isDbusSystem
+ final MultiAccountManager c, final boolean noReceiveOnStart, final boolean isDbusSystem
) throws UnexpectedErrorException {
runDbus(isDbusSystem, (connection, objectPath) -> {
final var signalControl = new DbusSignalControlImpl(c, objectPath);
import org.asamk.signal.JsonWriter;
import org.asamk.signal.commands.exceptions.CommandException;
+import org.asamk.signal.manager.MultiAccountManager;
public interface JsonRpcMultiCommand<T> extends JsonRpcCommand<T> {
- void handleCommand(T request, SignalCreator c, JsonWriter jsonWriter) throws CommandException;
+ void handleCommand(T request, MultiAccountManager c, JsonWriter jsonWriter) throws CommandException;
}
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputType;
import org.asamk.signal.commands.exceptions.CommandException;
+import org.asamk.signal.manager.MultiAccountManager;
import java.util.List;
import java.util.Map;
}
default void handleCommand(
- Map<String, Object> request, SignalCreator c, JsonWriter jsonWriter
+ Map<String, Object> request, MultiAccountManager c, JsonWriter jsonWriter
) throws CommandException {
Namespace commandNamespace = new JsonRpcNamespace(request == null ? Map.of() : request);
handleCommand(commandNamespace, c, jsonWriter);
}
try {
writer.println("{}", m.getDeviceLinkUri());
- try (var manager = m.finishDeviceLink(deviceName)) {
- writer.println("Associated with: {}", manager.getSelfNumber());
- }
+ var number = m.finishDeviceLink(deviceName);
+ writer.println("Associated with: {}", number);
} catch (TimeoutException e) {
throw new UserErrorException("Link request timed out, please try again.");
} catch (IOException e) {
import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.commands.exceptions.CommandException;
+import org.asamk.signal.manager.MultiAccountManager;
import java.util.stream.Collectors;
@Override
public void handleCommand(
- final Namespace ns, final SignalCreator c, final OutputWriter outputWriter
+ final Namespace ns, final MultiAccountManager c, final OutputWriter outputWriter
) throws CommandException {
final var accountNumbers = c.getAccountNumbers();
if (outputWriter instanceof JsonWriter jsonWriter) {
import org.asamk.signal.OutputWriter;
import org.asamk.signal.commands.exceptions.CommandException;
+import org.asamk.signal.manager.MultiAccountManager;
public interface MultiLocalCommand extends CliCommand {
- void handleCommand(Namespace ns, SignalCreator c, OutputWriter outputWriter) throws CommandException;
+ void handleCommand(Namespace ns, MultiAccountManager c, OutputWriter outputWriter) throws CommandException;
}
@Override
public void handleCommand(final Namespace ns, final Manager m, OutputWriter outputWriter) throws CommandException {
final var challenge = ns.getString("challenge");
- final var captchaString = ns.getString("captcha");
- final var captcha = captchaString == null ? null : captchaString.replace("signalcaptcha://", "");
+ final var captcha = ns.getString("captcha");
try {
m.submitRateLimitRecaptchaChallenge(challenge, captcha);
var pin = ns.getString("pin");
try {
- final var manager = m.verifyAccount(verificationCode, pin);
- manager.close();
+ m.verifyAccount(verificationCode, pin);
} catch (PinLockedException e) {
throw new UserErrorException(
"Verification failed! This number is locked with a pin. Hours remaining until reset: "
import org.asamk.signal.JsonWriter;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.MultiAccountManager;
import java.util.Map;
@Override
public void handleCommand(
- final Void request, final SignalCreator c, final JsonWriter jsonWriter
+ final Void request, final MultiAccountManager c, final JsonWriter jsonWriter
) throws CommandException {
outputVersion(jsonWriter);
}
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.MultiAccountManager;
import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.UserAlreadyExists;
public class DbusSignalControlImpl implements org.asamk.SignalControl {
- private final SignalCreator c;
+ private final MultiAccountManager c;
private final String objectPath;
- public DbusSignalControlImpl(final SignalCreator c, final String objectPath) {
+ public DbusSignalControlImpl(final MultiAccountManager c, final String objectPath) {
this.c = c;
this.objectPath = objectPath;
}
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);
- c.addManager(manager);
+ registrationManager.verifyAccount(verificationCode, pin);
} catch (IOException | PinLockedException | IncorrectPinException e) {
throw new SignalControl.Error.Failure(e.getClass().getSimpleName() + " " + e.getMessage());
}
final URI deviceLinkUri = provisioningManager.getDeviceLinkUri();
new Thread(() -> {
try {
- final Manager manager = provisioningManager.finishDeviceLink(newDeviceName);
- c.addManager(manager);
+ provisioningManager.finishDeviceLink(newDeviceName);
} catch (IOException | TimeoutException | UserAlreadyExists e) {
e.printStackTrace();
}
}
@Override
- public void submitRateLimitChallenge(String challenge, String captchaString) {
- final var captcha = captchaString == null ? null : captchaString.replace("signalcaptcha://", "");
-
+ public void submitRateLimitChallenge(String challenge, String captcha) {
try {
m.submitRateLimitRecaptchaChallenge(challenge, captcha);
} catch (IOException e) {
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ContainerNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import org.asamk.signal.JsonReceiveMessageHandler;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.commands.Commands;
import org.asamk.signal.commands.JsonRpcMultiCommand;
import org.asamk.signal.commands.JsonRpcSingleCommand;
-import org.asamk.signal.commands.SignalCreator;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.MultiAccountManager;
import org.asamk.signal.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private final JsonRpcReader jsonRpcReader;
private final boolean noReceiveOnStart;
- private SignalCreator c;
+ private MultiAccountManager c;
private final Map<Manager, Manager.ReceiveMessageHandler> receiveHandlers = new HashMap<>();
private Manager m;
this.jsonRpcReader = new JsonRpcReader(jsonRpcSender, lineSupplier);
}
- public void handleConnection(final SignalCreator c) {
+ public void handleConnection(final MultiAccountManager c) {
this.c = c;
if (!noReceiveOnStart) {
) throws JsonRpcException {
var command = getCommand(method);
// TODO implement register, verify, link
- if (c != null && command instanceof JsonRpcMultiCommand<?> jsonRpcCommand) {
- return runCommand(objectMapper, params, new MultiCommandRunnerImpl<>(c, jsonRpcCommand));
+ if (c != null) {
+ if (command instanceof JsonRpcMultiCommand<?> jsonRpcCommand) {
+ return runCommand(objectMapper, params, new MultiCommandRunnerImpl<>(c, jsonRpcCommand));
+ }
}
if (command instanceof JsonRpcSingleCommand<?> jsonRpcCommand) {
if (m != null) {
return runCommand(objectMapper, params, new CommandRunnerImpl<>(m, jsonRpcCommand));
}
- if (params.has("account")) {
- Manager manager = c.getManager(params.get("account").asText());
- if (manager != null) {
- return runCommand(objectMapper, params, new CommandRunnerImpl<>(manager, jsonRpcCommand));
- }
+ final var manager = getManagerFromParams(params);
+ if (manager != null) {
+ return runCommand(objectMapper, params, new CommandRunnerImpl<>(manager, jsonRpcCommand));
} else {
throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_PARAMS,
"Method requires valid account parameter",
null));
}
+ private Manager getManagerFromParams(final ContainerNode<?> params) {
+ if (params.has("account")) {
+ final var manager = c.getManager(params.get("account").asText());
+ ((ObjectNode) params).remove("account");
+ return manager;
+ }
+ return null;
+ }
+
private Command getCommand(final String method) {
if ("subscribeReceive".equals(method)) {
return new SubscribeReceiveCommand();
}
private record MultiCommandRunnerImpl<T>(
- SignalCreator c, JsonRpcMultiCommand<T> command
+ MultiAccountManager c, JsonRpcMultiCommand<T> command
) implements CommandRunner<T> {
@Override