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,
|
||||
"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",
|
||||
"allDeclaredFields":true,
|
||||
|
|
|
@ -34,7 +34,7 @@ public class SignalAccountFiles {
|
|||
final ServiceEnvironment serviceEnvironment,
|
||||
final String userAgent,
|
||||
final TrustNewIdentity trustNewIdentity
|
||||
) {
|
||||
) throws IOException {
|
||||
this.pathConfig = PathConfig.createDefault(settingsPath);
|
||||
this.serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, 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;
|
||||
|
||||
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.util.PhoneNumberFormatter;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
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.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class AccountsStore {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(AccountsStore.class);
|
||||
private final ObjectMapper objectMapper = Utils.createStorageObjectMapper();
|
||||
|
||||
private final File dataPath;
|
||||
|
||||
public AccountsStore(final File dataPath) {
|
||||
public AccountsStore(final File dataPath) throws IOException {
|
||||
this.dataPath = dataPath;
|
||||
if (!getAccountsFile().exists()) {
|
||||
createInitialAccounts();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if (files == null) {
|
||||
|
@ -30,23 +144,61 @@ public class AccountsStore {
|
|||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public String getPathByNumber(String number) {
|
||||
return number;
|
||||
private List<AccountsStorage.Account> readAccounts() {
|
||||
try {
|
||||
final var pair = openFileChannel(getAccountsFile());
|
||||
try (final var fileChannel = pair.first(); final var lock = pair.second()) {
|
||||
return readAccountsLocked(fileChannel).accounts();
|
||||
}
|
||||
|
||||
public String getPathByAci(ACI aci) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void updateAccount(String path, String number, ACI aci) {
|
||||
// TODO remove number and uuid from all other accounts
|
||||
if (!path.equals(number)) {
|
||||
throw new UnsupportedOperationException("Updating number not supported yet");
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to read accounts list", e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
public String addAccount(String number, ACI aci) {
|
||||
// TODO remove number and uuid from all other accounts
|
||||
return number;
|
||||
private void updateAccounts(Function<List<AccountsStorage.Account>, List<AccountsStorage.Account>> updater) {
|
||||
try {
|
||||
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
|
||||
: trustNewIdentityCli == TrustNewIdentityCli.ALWAYS ? TrustNewIdentity.ALWAYS : TrustNewIdentity.NEVER;
|
||||
|
||||
final SignalAccountFiles signalAccountFiles = new SignalAccountFiles(configPath,
|
||||
final SignalAccountFiles signalAccountFiles;
|
||||
try {
|
||||
signalAccountFiles = new SignalAccountFiles(configPath,
|
||||
serviceEnvironment,
|
||||
BaseConfig.USER_AGENT,
|
||||
trustNewIdentity);
|
||||
} catch (IOException e) {
|
||||
throw new IOErrorException("Failed to read local accounts list", e);
|
||||
}
|
||||
|
||||
if (command instanceof ProvisioningCommand provisioningCommand) {
|
||||
if (account != null) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue