Allow registering new accounts on both live and staging environments

in the same config directory
This commit is contained in:
AsamK 2022-06-11 22:45:51 +02:00
parent c487929bcd
commit aaa6412469
5 changed files with 136 additions and 35 deletions

View file

@ -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":[] }

View file

@ -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;
} }
public Set<String> getAllLocalAccountNumbers() { try {
return SignalAccount.load(pathConfig.dataPath(), accountPath, false, trustNewIdentity);
} catch (Exception e) {
return null;
}
});
}
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;

View file

@ -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) {}
} }

View file

@ -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()) {
return readAccountsLocked(fileChannel).accounts(); final var storage = readAccountsLocked(fileChannel);
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);
}
} }

View file

@ -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);
} }
} }