]> nmode's Git Repositories - signal-cli/commitdiff
Store account list in accounts.json file
authorAsamK <asamk@gmx.de>
Thu, 10 Feb 2022 17:27:24 +0000 (18:27 +0100)
committerAsamK <asamk@gmx.de>
Fri, 11 Feb 2022 20:03:54 +0000 (21:03 +0100)
graalvm-config-dir/reflect-config.json
lib/src/main/java/org/asamk/signal/manager/SignalAccountFiles.java
lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStorage.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java
src/main/java/org/asamk/signal/App.java

index ae98cd74c47463dfbc36d596b60736fe98c2445a..158f94294645b1909076811188a2018f5285344e 100644 (file)
   "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,
index f1d37e81acbd9f2fd640d11fdca5047e74ff8ada..379aa1cb5ef79f5ff5e030e142f7fa14024a1515 100644 (file)
@@ -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;
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStorage.java b/lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStorage.java
new file mode 100644 (file)
index 0000000..341d957
--- /dev/null
@@ -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) {}
+}
index ed6dec9701e27ce6d146ab0d95f08589e56f7040..07dfac3c766ba95ef3af8ef69e817d74dcb5d564 100644 (file)
 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();
+            }
+        } catch (IOException e) {
+            logger.error("Failed to read accounts list", e);
+            return List.of();
+        }
     }
 
-    public String getPathByAci(ACI aci) {
-        return null;
+    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);
+        }
     }
 
-    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");
+    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);
         }
     }
 
-    public String addAccount(String number, ACI aci) {
-        // TODO remove number and uuid from all other accounts
-        return number;
+    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);
     }
 }
index 5853e4b8322bec2773d73ad2196e909d805aea1d..7a9297a8ccabd1ce59ce5cfa75bfcc54e3833c72 100644 (file)
@@ -163,10 +163,15 @@ public class App {
                 ? TrustNewIdentity.ON_FIRST_USE
                 : trustNewIdentityCli == TrustNewIdentityCli.ALWAYS ? TrustNewIdentity.ALWAYS : TrustNewIdentity.NEVER;
 
-        final SignalAccountFiles signalAccountFiles = new SignalAccountFiles(configPath,
-                serviceEnvironment,
-                BaseConfig.USER_AGENT,
-                trustNewIdentity);
+        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) {