mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Store account list in accounts.json file
This commit is contained in:
parent
ff6b733cd0
commit
0476895c3d
5 changed files with 208 additions and 21 deletions
|
@ -859,6 +859,28 @@
|
||||||
"allDeclaredFields":true,
|
"allDeclaredFields":true,
|
||||||
"queryAllDeclaredMethods":true
|
"queryAllDeclaredMethods":true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"org.asamk.signal.manager.storage.accounts.AccountsStorage",
|
||||||
|
"allDeclaredFields":true,
|
||||||
|
"queryAllDeclaredMethods":true,
|
||||||
|
"queryAllDeclaredConstructors":true,
|
||||||
|
"methods":[
|
||||||
|
{"name":"<init>","parameterTypes":["java.util.List"] },
|
||||||
|
{"name":"accounts","parameterTypes":[] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.asamk.signal.manager.storage.accounts.AccountsStorage$Account",
|
||||||
|
"allDeclaredFields":true,
|
||||||
|
"queryAllDeclaredMethods":true,
|
||||||
|
"queryAllDeclaredConstructors":true,
|
||||||
|
"methods":[
|
||||||
|
{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","java.lang.String"] },
|
||||||
|
{"name":"number","parameterTypes":[] },
|
||||||
|
{"name":"path","parameterTypes":[] },
|
||||||
|
{"name":"uuid","parameterTypes":[] }
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"org.asamk.signal.manager.storage.configuration.ConfigurationStore$Storage",
|
"name":"org.asamk.signal.manager.storage.configuration.ConfigurationStore$Storage",
|
||||||
"allDeclaredFields":true,
|
"allDeclaredFields":true,
|
||||||
|
|
|
@ -34,7 +34,7 @@ public class SignalAccountFiles {
|
||||||
final ServiceEnvironment serviceEnvironment,
|
final ServiceEnvironment serviceEnvironment,
|
||||||
final String userAgent,
|
final String userAgent,
|
||||||
final TrustNewIdentity trustNewIdentity
|
final TrustNewIdentity trustNewIdentity
|
||||||
) {
|
) throws IOException {
|
||||||
this.pathConfig = PathConfig.createDefault(settingsPath);
|
this.pathConfig = PathConfig.createDefault(settingsPath);
|
||||||
this.serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
|
this.serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package org.asamk.signal.manager.storage.accounts;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
record AccountsStorage(List<Account> accounts) {
|
||||||
|
|
||||||
|
record Account(String path, String number, String uuid) {}
|
||||||
|
}
|
|
@ -1,22 +1,136 @@
|
||||||
package org.asamk.signal.manager.storage.accounts;
|
package org.asamk.signal.manager.storage.accounts;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.api.Pair;
|
||||||
|
import org.asamk.signal.manager.storage.Utils;
|
||||||
|
import org.asamk.signal.manager.util.IOUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.push.ACI;
|
import org.whispersystems.signalservice.api.push.ACI;
|
||||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.nio.channels.FileLock;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Random;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public class AccountsStore {
|
public class AccountsStore {
|
||||||
|
|
||||||
|
private final static Logger logger = LoggerFactory.getLogger(AccountsStore.class);
|
||||||
|
private final ObjectMapper objectMapper = Utils.createStorageObjectMapper();
|
||||||
|
|
||||||
private final File dataPath;
|
private final File dataPath;
|
||||||
|
|
||||||
public AccountsStore(final File dataPath) {
|
public AccountsStore(final File dataPath) throws IOException {
|
||||||
this.dataPath = dataPath;
|
this.dataPath = dataPath;
|
||||||
|
if (!getAccountsFile().exists()) {
|
||||||
|
createInitialAccounts();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<String> getAllNumbers() {
|
public Set<String> getAllNumbers() {
|
||||||
|
return readAccounts().stream()
|
||||||
|
.map(AccountsStorage.Account::number)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPathByNumber(String number) {
|
||||||
|
return readAccounts().stream()
|
||||||
|
.filter(a -> number.equals(a.number()))
|
||||||
|
.map(AccountsStorage.Account::path)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPathByAci(ACI aci) {
|
||||||
|
return readAccounts().stream()
|
||||||
|
.filter(a -> aci.toString().equals(a.uuid()))
|
||||||
|
.map(AccountsStorage.Account::path)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateAccount(String path, String number, ACI aci) {
|
||||||
|
updateAccounts(accounts -> accounts.stream().map(a -> {
|
||||||
|
if (path.equals(a.path())) {
|
||||||
|
return new AccountsStorage.Account(a.path(), number, aci == null ? null : aci.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (number != null && number.equals(a.number())) {
|
||||||
|
return new AccountsStorage.Account(a.path(), null, a.uuid());
|
||||||
|
}
|
||||||
|
if (aci != null && aci.toString().equals(a.toString())) {
|
||||||
|
return new AccountsStorage.Account(a.path(), a.number(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return a;
|
||||||
|
}).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String addAccount(String number, ACI aci) {
|
||||||
|
final var accountPath = generateNewAccountPath();
|
||||||
|
final var account = new AccountsStorage.Account(accountPath, number, aci == null ? null : aci.toString());
|
||||||
|
updateAccounts(accounts -> {
|
||||||
|
final var existingAccounts = accounts.stream().map(a -> {
|
||||||
|
if (number != null && number.equals(a.number())) {
|
||||||
|
return new AccountsStorage.Account(a.path(), null, a.uuid());
|
||||||
|
}
|
||||||
|
if (aci != null && aci.toString().equals(a.toString())) {
|
||||||
|
return new AccountsStorage.Account(a.path(), a.number(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return a;
|
||||||
|
});
|
||||||
|
return Stream.concat(existingAccounts, Stream.of(account)).toList();
|
||||||
|
});
|
||||||
|
return accountPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateNewAccountPath() {
|
||||||
|
return new Random().ints(100000, 1000000)
|
||||||
|
.mapToObj(String::valueOf)
|
||||||
|
.filter(n -> !new File(dataPath, n).exists() && !new File(dataPath, n + ".d").exists())
|
||||||
|
.findFirst()
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getAccountsFile() {
|
||||||
|
return new File(dataPath, "accounts.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createInitialAccounts() throws IOException {
|
||||||
|
final var legacyAccountPaths = getLegacyAccountPaths();
|
||||||
|
final var accountsStorage = new AccountsStorage(legacyAccountPaths.stream()
|
||||||
|
.map(number -> new AccountsStorage.Account(number, number, null))
|
||||||
|
.toList());
|
||||||
|
|
||||||
|
IOUtils.createPrivateDirectories(dataPath);
|
||||||
|
var fileName = getAccountsFile();
|
||||||
|
if (!fileName.exists()) {
|
||||||
|
IOUtils.createPrivateFile(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
final var pair = openFileChannel(getAccountsFile());
|
||||||
|
try (final var fileChannel = pair.first(); final var lock = pair.second()) {
|
||||||
|
saveAccountsLocked(fileChannel, accountsStorage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> getLegacyAccountPaths() {
|
||||||
final var files = dataPath.listFiles();
|
final var files = dataPath.listFiles();
|
||||||
|
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
|
@ -30,23 +144,61 @@ public class AccountsStore {
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPathByNumber(String number) {
|
private List<AccountsStorage.Account> readAccounts() {
|
||||||
return number;
|
try {
|
||||||
}
|
final var pair = openFileChannel(getAccountsFile());
|
||||||
|
try (final var fileChannel = pair.first(); final var lock = pair.second()) {
|
||||||
public String getPathByAci(ACI aci) {
|
return readAccountsLocked(fileChannel).accounts();
|
||||||
return null;
|
}
|
||||||
}
|
} catch (IOException e) {
|
||||||
|
logger.error("Failed to read accounts list", e);
|
||||||
public void updateAccount(String path, String number, ACI aci) {
|
return List.of();
|
||||||
// TODO remove number and uuid from all other accounts
|
|
||||||
if (!path.equals(number)) {
|
|
||||||
throw new UnsupportedOperationException("Updating number not supported yet");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String addAccount(String number, ACI aci) {
|
private void updateAccounts(Function<List<AccountsStorage.Account>, List<AccountsStorage.Account>> updater) {
|
||||||
// TODO remove number and uuid from all other accounts
|
try {
|
||||||
return number;
|
final var pair = openFileChannel(getAccountsFile());
|
||||||
|
try (final var fileChannel = pair.first(); final var lock = pair.second()) {
|
||||||
|
final var accountsStorage = readAccountsLocked(fileChannel);
|
||||||
|
final var newAccountsStorage = updater.apply(accountsStorage.accounts());
|
||||||
|
saveAccountsLocked(fileChannel, new AccountsStorage(newAccountsStorage));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Failed to update accounts list", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AccountsStorage readAccountsLocked(FileChannel fileChannel) throws IOException {
|
||||||
|
fileChannel.position(0);
|
||||||
|
final var inputStream = Channels.newInputStream(fileChannel);
|
||||||
|
return objectMapper.readValue(inputStream, AccountsStorage.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveAccountsLocked(FileChannel fileChannel, AccountsStorage accountsStorage) throws IOException {
|
||||||
|
try {
|
||||||
|
try (var output = new ByteArrayOutputStream()) {
|
||||||
|
// Write to memory first to prevent corrupting the file in case of serialization errors
|
||||||
|
objectMapper.writeValue(output, accountsStorage);
|
||||||
|
var input = new ByteArrayInputStream(output.toByteArray());
|
||||||
|
fileChannel.position(0);
|
||||||
|
input.transferTo(Channels.newOutputStream(fileChannel));
|
||||||
|
fileChannel.truncate(fileChannel.position());
|
||||||
|
fileChannel.force(false);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error saving accounts file: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pair<FileChannel, FileLock> openFileChannel(File fileName) throws IOException {
|
||||||
|
var fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
|
||||||
|
var lock = fileChannel.tryLock();
|
||||||
|
if (lock == null) {
|
||||||
|
logger.info("Config file is in use by another instance, waiting…");
|
||||||
|
lock = fileChannel.lock();
|
||||||
|
logger.info("Config file lock acquired.");
|
||||||
|
}
|
||||||
|
return new Pair<>(fileChannel, lock);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,10 +163,15 @@ public class App {
|
||||||
? TrustNewIdentity.ON_FIRST_USE
|
? TrustNewIdentity.ON_FIRST_USE
|
||||||
: trustNewIdentityCli == TrustNewIdentityCli.ALWAYS ? TrustNewIdentity.ALWAYS : TrustNewIdentity.NEVER;
|
: trustNewIdentityCli == TrustNewIdentityCli.ALWAYS ? TrustNewIdentity.ALWAYS : TrustNewIdentity.NEVER;
|
||||||
|
|
||||||
final SignalAccountFiles signalAccountFiles = new SignalAccountFiles(configPath,
|
final SignalAccountFiles signalAccountFiles;
|
||||||
serviceEnvironment,
|
try {
|
||||||
BaseConfig.USER_AGENT,
|
signalAccountFiles = new SignalAccountFiles(configPath,
|
||||||
trustNewIdentity);
|
serviceEnvironment,
|
||||||
|
BaseConfig.USER_AGENT,
|
||||||
|
trustNewIdentity);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOErrorException("Failed to read local accounts list", e);
|
||||||
|
}
|
||||||
|
|
||||||
if (command instanceof ProvisioningCommand provisioningCommand) {
|
if (command instanceof ProvisioningCommand provisioningCommand) {
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue