mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Allow registering new accounts on both live and staging environments
in the same config directory
This commit is contained in:
parent
c487929bcd
commit
aaa6412469
5 changed files with 136 additions and 35 deletions
|
@ -914,8 +914,9 @@
|
||||||
"queryAllDeclaredMethods":true,
|
"queryAllDeclaredMethods":true,
|
||||||
"queryAllDeclaredConstructors":true,
|
"queryAllDeclaredConstructors":true,
|
||||||
"methods":[
|
"methods":[
|
||||||
{"name":"<init>","parameterTypes":["java.util.List"] },
|
{"name":"<init>","parameterTypes":["java.util.List","java.lang.Integer"] },
|
||||||
{"name":"accounts","parameterTypes":[] }
|
{"name":"accounts","parameterTypes":[] },
|
||||||
|
{"name":"version","parameterTypes":[] }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -924,7 +925,8 @@
|
||||||
"queryAllDeclaredMethods":true,
|
"queryAllDeclaredMethods":true,
|
||||||
"queryAllDeclaredConstructors":true,
|
"queryAllDeclaredConstructors":true,
|
||||||
"methods":[
|
"methods":[
|
||||||
{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","java.lang.String"] },
|
{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String"] },
|
||||||
|
{"name":"environment","parameterTypes":[] },
|
||||||
{"name":"number","parameterTypes":[] },
|
{"name":"number","parameterTypes":[] },
|
||||||
{"name":"path","parameterTypes":[] },
|
{"name":"path","parameterTypes":[] },
|
||||||
{"name":"uuid","parameterTypes":[] }
|
{"name":"uuid","parameterTypes":[] }
|
||||||
|
|
|
@ -41,14 +41,24 @@ public class SignalAccountFiles {
|
||||||
this.serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(this.serviceEnvironment, userAgent);
|
this.serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(this.serviceEnvironment, userAgent);
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
this.trustNewIdentity = trustNewIdentity;
|
this.trustNewIdentity = trustNewIdentity;
|
||||||
this.accountsStore = new AccountsStore(pathConfig.dataPath());
|
this.accountsStore = new AccountsStore(pathConfig.dataPath(), serviceEnvironment, accountPath -> {
|
||||||
|
if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return SignalAccount.load(pathConfig.dataPath(), accountPath, false, trustNewIdentity);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<String> getAllLocalAccountNumbers() {
|
public Set<String> getAllLocalAccountNumbers() throws IOException {
|
||||||
return accountsStore.getAllNumbers();
|
return accountsStore.getAllNumbers();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MultiAccountManager initMultiAccountManager() {
|
public MultiAccountManager initMultiAccountManager() throws IOException {
|
||||||
final var managers = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
final var managers = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
||||||
try {
|
try {
|
||||||
return initManager(a.number(), a.path());
|
return initManager(a.number(), a.path());
|
||||||
|
@ -108,6 +118,7 @@ public class SignalAccountFiles {
|
||||||
|
|
||||||
if (account.getServiceEnvironment() == null) {
|
if (account.getServiceEnvironment() == null) {
|
||||||
account.setServiceEnvironment(serviceEnvironment);
|
account.setServiceEnvironment(serviceEnvironment);
|
||||||
|
accountsStore.updateAccount(accountPath, account.getNumber(), account.getAci());
|
||||||
}
|
}
|
||||||
|
|
||||||
return manager;
|
return manager;
|
||||||
|
|
|
@ -2,7 +2,7 @@ package org.asamk.signal.manager.storage.accounts;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public record AccountsStorage(List<Account> accounts) {
|
public record AccountsStorage(List<Account> accounts, Integer version) {
|
||||||
|
|
||||||
public record Account(String path, String number, String uuid) {}
|
public record Account(String path, String environment, String number, String uuid) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package org.asamk.signal.manager.storage.accounts;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.Pair;
|
import org.asamk.signal.manager.api.Pair;
|
||||||
|
import org.asamk.signal.manager.config.ServiceEnvironment;
|
||||||
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.Utils;
|
import org.asamk.signal.manager.storage.Utils;
|
||||||
import org.asamk.signal.manager.util.IOUtils;
|
import org.asamk.signal.manager.util.IOUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -29,39 +31,52 @@ import java.util.stream.Stream;
|
||||||
|
|
||||||
public class AccountsStore {
|
public class AccountsStore {
|
||||||
|
|
||||||
|
private static final int MINIMUM_STORAGE_VERSION = 1;
|
||||||
|
private static final int CURRENT_STORAGE_VERSION = 2;
|
||||||
private final static Logger logger = LoggerFactory.getLogger(AccountsStore.class);
|
private final static Logger logger = LoggerFactory.getLogger(AccountsStore.class);
|
||||||
private final ObjectMapper objectMapper = Utils.createStorageObjectMapper();
|
private final ObjectMapper objectMapper = Utils.createStorageObjectMapper();
|
||||||
|
|
||||||
private final File dataPath;
|
private final File dataPath;
|
||||||
|
private final String serviceEnvironment;
|
||||||
|
private final AccountLoader accountLoader;
|
||||||
|
|
||||||
public AccountsStore(final File dataPath) throws IOException {
|
public AccountsStore(
|
||||||
|
final File dataPath, final ServiceEnvironment serviceEnvironment, final AccountLoader accountLoader
|
||||||
|
) throws IOException {
|
||||||
this.dataPath = dataPath;
|
this.dataPath = dataPath;
|
||||||
|
this.serviceEnvironment = getServiceEnvironmentString(serviceEnvironment);
|
||||||
|
this.accountLoader = accountLoader;
|
||||||
if (!getAccountsFile().exists()) {
|
if (!getAccountsFile().exists()) {
|
||||||
createInitialAccounts();
|
createInitialAccounts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized Set<String> getAllNumbers() {
|
public synchronized Set<String> getAllNumbers() throws IOException {
|
||||||
return readAccounts().stream()
|
return readAccounts().stream()
|
||||||
.map(AccountsStorage.Account::number)
|
.map(AccountsStorage.Account::number)
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized Set<AccountsStorage.Account> getAllAccounts() {
|
public synchronized Set<AccountsStorage.Account> getAllAccounts() throws IOException {
|
||||||
return readAccounts().stream().filter(a -> a.number() != null).collect(Collectors.toSet());
|
return readAccounts().stream()
|
||||||
|
.filter(a -> a.environment() == null || serviceEnvironment.equals(a.environment()))
|
||||||
|
.filter(a -> a.number() != null)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized String getPathByNumber(String number) {
|
public synchronized String getPathByNumber(String number) throws IOException {
|
||||||
return readAccounts().stream()
|
return readAccounts().stream()
|
||||||
|
.filter(a -> a.environment() == null || serviceEnvironment.equals(a.environment()))
|
||||||
.filter(a -> number.equals(a.number()))
|
.filter(a -> number.equals(a.number()))
|
||||||
.map(AccountsStorage.Account::path)
|
.map(AccountsStorage.Account::path)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized String getPathByAci(ACI aci) {
|
public synchronized String getPathByAci(ACI aci) throws IOException {
|
||||||
return readAccounts().stream()
|
return readAccounts().stream()
|
||||||
|
.filter(a -> a.environment() == null || serviceEnvironment.equals(a.environment()))
|
||||||
.filter(a -> aci.toString().equals(a.uuid()))
|
.filter(a -> aci.toString().equals(a.uuid()))
|
||||||
.map(AccountsStorage.Account::path)
|
.map(AccountsStorage.Account::path)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
|
@ -70,15 +85,22 @@ public class AccountsStore {
|
||||||
|
|
||||||
public synchronized void updateAccount(String path, String number, ACI aci) {
|
public synchronized void updateAccount(String path, String number, ACI aci) {
|
||||||
updateAccounts(accounts -> accounts.stream().map(a -> {
|
updateAccounts(accounts -> accounts.stream().map(a -> {
|
||||||
|
if (a.environment() != null && !serviceEnvironment.equals(a.environment())) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
if (path.equals(a.path())) {
|
if (path.equals(a.path())) {
|
||||||
return new AccountsStorage.Account(a.path(), number, aci == null ? null : aci.toString());
|
return new AccountsStorage.Account(a.path(),
|
||||||
|
serviceEnvironment,
|
||||||
|
number,
|
||||||
|
aci == null ? null : aci.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (number != null && number.equals(a.number())) {
|
if (number != null && number.equals(a.number())) {
|
||||||
return new AccountsStorage.Account(a.path(), null, a.uuid());
|
return new AccountsStorage.Account(a.path(), a.environment(), null, a.uuid());
|
||||||
}
|
}
|
||||||
if (aci != null && aci.toString().equals(a.toString())) {
|
if (aci != null && aci.toString().equals(a.toString())) {
|
||||||
return new AccountsStorage.Account(a.path(), a.number(), null);
|
return new AccountsStorage.Account(a.path(), a.environment(), a.number(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return a;
|
return a;
|
||||||
|
@ -87,14 +109,21 @@ public class AccountsStore {
|
||||||
|
|
||||||
public synchronized String addAccount(String number, ACI aci) {
|
public synchronized String addAccount(String number, ACI aci) {
|
||||||
final var accountPath = generateNewAccountPath();
|
final var accountPath = generateNewAccountPath();
|
||||||
final var account = new AccountsStorage.Account(accountPath, number, aci == null ? null : aci.toString());
|
final var account = new AccountsStorage.Account(accountPath,
|
||||||
|
serviceEnvironment,
|
||||||
|
number,
|
||||||
|
aci == null ? null : aci.toString());
|
||||||
updateAccounts(accounts -> {
|
updateAccounts(accounts -> {
|
||||||
final var existingAccounts = accounts.stream().map(a -> {
|
final var existingAccounts = accounts.stream().map(a -> {
|
||||||
if (number != null && number.equals(a.number())) {
|
if (a.environment() != null && !serviceEnvironment.equals(a.environment())) {
|
||||||
return new AccountsStorage.Account(a.path(), null, a.uuid());
|
return a;
|
||||||
}
|
}
|
||||||
if (aci != null && aci.toString().equals(a.toString())) {
|
|
||||||
return new AccountsStorage.Account(a.path(), a.number(), null);
|
if (number != null && number.equals(a.number())) {
|
||||||
|
return new AccountsStorage.Account(a.path(), a.environment(), null, a.uuid());
|
||||||
|
}
|
||||||
|
if (aci != null && aci.toString().equals(a.uuid())) {
|
||||||
|
return new AccountsStorage.Account(a.path(), a.environment(), a.number(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return a;
|
return a;
|
||||||
|
@ -105,7 +134,9 @@ public class AccountsStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeAccount(final String accountPath) {
|
public void removeAccount(final String accountPath) {
|
||||||
updateAccounts(accounts -> accounts.stream().filter(a -> !a.path().equals(accountPath)).toList());
|
updateAccounts(accounts -> accounts.stream().filter(a -> !(
|
||||||
|
(a.environment() == null || serviceEnvironment.equals(a.environment())) && a.path().equals(accountPath)
|
||||||
|
)).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateNewAccountPath() {
|
private String generateNewAccountPath() {
|
||||||
|
@ -123,8 +154,8 @@ public class AccountsStore {
|
||||||
private void createInitialAccounts() throws IOException {
|
private void createInitialAccounts() throws IOException {
|
||||||
final var legacyAccountPaths = getLegacyAccountPaths();
|
final var legacyAccountPaths = getLegacyAccountPaths();
|
||||||
final var accountsStorage = new AccountsStorage(legacyAccountPaths.stream()
|
final var accountsStorage = new AccountsStorage(legacyAccountPaths.stream()
|
||||||
.map(number -> new AccountsStorage.Account(number, number, null))
|
.map(number -> new AccountsStorage.Account(number, null, number, null))
|
||||||
.toList());
|
.toList(), CURRENT_STORAGE_VERSION);
|
||||||
|
|
||||||
IOUtils.createPrivateDirectories(dataPath);
|
IOUtils.createPrivateDirectories(dataPath);
|
||||||
var fileName = getAccountsFile();
|
var fileName = getAccountsFile();
|
||||||
|
@ -152,15 +183,52 @@ public class AccountsStore {
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<AccountsStorage.Account> readAccounts() {
|
private List<AccountsStorage.Account> readAccounts() throws IOException {
|
||||||
try {
|
final var pair = openFileChannel(getAccountsFile());
|
||||||
final var pair = openFileChannel(getAccountsFile());
|
try (final var fileChannel = pair.first(); final var lock = pair.second()) {
|
||||||
try (final var fileChannel = pair.first(); final var lock = pair.second()) {
|
final var storage = readAccountsLocked(fileChannel);
|
||||||
return readAccountsLocked(fileChannel).accounts();
|
|
||||||
|
var accountsVersion = storage.version() == null ? 1 : storage.version();
|
||||||
|
if (accountsVersion > CURRENT_STORAGE_VERSION) {
|
||||||
|
throw new IOException("Accounts file was created by a more recent version: " + accountsVersion);
|
||||||
|
} else if (accountsVersion < MINIMUM_STORAGE_VERSION) {
|
||||||
|
throw new IOException("Accounts file was created by a no longer supported older version: "
|
||||||
|
+ accountsVersion);
|
||||||
|
} else if (accountsVersion < CURRENT_STORAGE_VERSION) {
|
||||||
|
return upgradeAccountsFile(fileChannel, storage, accountsVersion).accounts();
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
return storage.accounts();
|
||||||
logger.error("Failed to read accounts list", e);
|
}
|
||||||
return List.of();
|
}
|
||||||
|
|
||||||
|
private AccountsStorage upgradeAccountsFile(
|
||||||
|
final FileChannel fileChannel, final AccountsStorage storage, final int accountsVersion
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
List<AccountsStorage.Account> newAccounts = storage.accounts();
|
||||||
|
if (accountsVersion < 2) {
|
||||||
|
// add environment field
|
||||||
|
newAccounts = newAccounts.stream().map(a -> {
|
||||||
|
if (a.environment() != null) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
try (final var account = accountLoader.loadAccountOrNull(a.path())) {
|
||||||
|
if (account == null || account.getServiceEnvironment() == null) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
return new AccountsStorage.Account(a.path(),
|
||||||
|
getServiceEnvironmentString(account.getServiceEnvironment()),
|
||||||
|
a.number(),
|
||||||
|
a.uuid());
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
final var newStorage = new AccountsStorage(newAccounts, CURRENT_STORAGE_VERSION);
|
||||||
|
saveAccountsLocked(fileChannel, newStorage);
|
||||||
|
return newStorage;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Failed to upgrade accounts file", e);
|
||||||
|
return storage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,7 +238,7 @@ public class AccountsStore {
|
||||||
try (final var fileChannel = pair.first(); final var lock = pair.second()) {
|
try (final var fileChannel = pair.first(); final var lock = pair.second()) {
|
||||||
final var accountsStorage = readAccountsLocked(fileChannel);
|
final var accountsStorage = readAccountsLocked(fileChannel);
|
||||||
final var newAccountsStorage = updater.apply(accountsStorage.accounts());
|
final var newAccountsStorage = updater.apply(accountsStorage.accounts());
|
||||||
saveAccountsLocked(fileChannel, new AccountsStorage(newAccountsStorage));
|
saveAccountsLocked(fileChannel, new AccountsStorage(newAccountsStorage, CURRENT_STORAGE_VERSION));
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("Failed to update accounts list", e);
|
logger.error("Failed to update accounts list", e);
|
||||||
|
@ -209,4 +277,16 @@ public class AccountsStore {
|
||||||
}
|
}
|
||||||
return new Pair<>(fileChannel, lock);
|
return new Pair<>(fileChannel, lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getServiceEnvironmentString(final ServiceEnvironment serviceEnvironment) {
|
||||||
|
return switch (serviceEnvironment) {
|
||||||
|
case LIVE -> "LIVE";
|
||||||
|
case STAGING -> "STAGING";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface AccountLoader {
|
||||||
|
|
||||||
|
SignalAccount loadAccountOrNull(String accountPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ import java.io.BufferedWriter;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import static net.sourceforge.argparse4j.DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS;
|
import static net.sourceforge.argparse4j.DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS;
|
||||||
|
|
||||||
|
@ -188,7 +189,12 @@ public class App {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var accounts = signalAccountFiles.getAllLocalAccountNumbers();
|
Set<String> accounts = null;
|
||||||
|
try {
|
||||||
|
accounts = signalAccountFiles.getAllLocalAccountNumbers();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOErrorException("Failed to load local accounts file", e);
|
||||||
|
}
|
||||||
if (accounts.size() == 0) {
|
if (accounts.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");
|
||||||
} else if (accounts.size() > 1) {
|
} else if (accounts.size() > 1) {
|
||||||
|
@ -299,6 +305,8 @@ public class App {
|
||||||
) throws CommandException {
|
) throws CommandException {
|
||||||
try (var multiAccountManager = signalAccountFiles.initMultiAccountManager()) {
|
try (var multiAccountManager = signalAccountFiles.initMultiAccountManager()) {
|
||||||
command.handleCommand(ns, multiAccountManager, outputWriter);
|
command.handleCommand(ns, multiAccountManager, outputWriter);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOErrorException("Failed to load local accounts file", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue