1 package org
.asamk
.signal
.manager
.storage
.accounts
;
3 import com
.fasterxml
.jackson
.databind
.ObjectMapper
;
5 import org
.asamk
.signal
.manager
.api
.Pair
;
6 import org
.asamk
.signal
.manager
.storage
.Utils
;
7 import org
.asamk
.signal
.manager
.util
.IOUtils
;
8 import org
.slf4j
.Logger
;
9 import org
.slf4j
.LoggerFactory
;
10 import org
.whispersystems
.signalservice
.api
.push
.ACI
;
11 import org
.whispersystems
.signalservice
.api
.util
.PhoneNumberFormatter
;
13 import java
.io
.ByteArrayInputStream
;
14 import java
.io
.ByteArrayOutputStream
;
16 import java
.io
.IOException
;
17 import java
.io
.RandomAccessFile
;
18 import java
.nio
.channels
.Channels
;
19 import java
.nio
.channels
.FileChannel
;
20 import java
.nio
.channels
.FileLock
;
21 import java
.util
.Arrays
;
22 import java
.util
.List
;
23 import java
.util
.Objects
;
24 import java
.util
.Random
;
26 import java
.util
.function
.Function
;
27 import java
.util
.stream
.Collectors
;
28 import java
.util
.stream
.Stream
;
30 public class AccountsStore
{
32 private final static Logger logger
= LoggerFactory
.getLogger(AccountsStore
.class);
33 private final ObjectMapper objectMapper
= Utils
.createStorageObjectMapper();
35 private final File dataPath
;
37 public AccountsStore(final File dataPath
) throws IOException
{
38 this.dataPath
= dataPath
;
39 if (!getAccountsFile().exists()) {
40 createInitialAccounts();
44 public synchronized Set
<String
> getAllNumbers() {
45 return readAccounts().stream()
46 .map(AccountsStorage
.Account
::number
)
47 .filter(Objects
::nonNull
)
48 .collect(Collectors
.toSet());
51 public synchronized Set
<AccountsStorage
.Account
> getAllAccounts() {
52 return readAccounts().stream().filter(a
-> a
.number() != null).collect(Collectors
.toSet());
55 public synchronized String
getPathByNumber(String number
) {
56 return readAccounts().stream()
57 .filter(a
-> number
.equals(a
.number()))
58 .map(AccountsStorage
.Account
::path
)
63 public synchronized String
getPathByAci(ACI aci
) {
64 return readAccounts().stream()
65 .filter(a
-> aci
.toString().equals(a
.uuid()))
66 .map(AccountsStorage
.Account
::path
)
71 public synchronized void updateAccount(String path
, String number
, ACI aci
) {
72 updateAccounts(accounts
-> accounts
.stream().map(a
-> {
73 if (path
.equals(a
.path())) {
74 return new AccountsStorage
.Account(a
.path(), number
, aci
== null ?
null : aci
.toString());
77 if (number
!= null && number
.equals(a
.number())) {
78 return new AccountsStorage
.Account(a
.path(), null, a
.uuid());
80 if (aci
!= null && aci
.toString().equals(a
.toString())) {
81 return new AccountsStorage
.Account(a
.path(), a
.number(), null);
88 public synchronized String
addAccount(String number
, ACI aci
) {
89 final var accountPath
= generateNewAccountPath();
90 final var account
= new AccountsStorage
.Account(accountPath
, number
, aci
== null ?
null : aci
.toString());
91 updateAccounts(accounts
-> {
92 final var existingAccounts
= accounts
.stream().map(a
-> {
93 if (number
!= null && number
.equals(a
.number())) {
94 return new AccountsStorage
.Account(a
.path(), null, a
.uuid());
96 if (aci
!= null && aci
.toString().equals(a
.toString())) {
97 return new AccountsStorage
.Account(a
.path(), a
.number(), null);
102 return Stream
.concat(existingAccounts
, Stream
.of(account
)).toList();
107 private String
generateNewAccountPath() {
108 return new Random().ints(100000, 1000000)
109 .mapToObj(String
::valueOf
)
110 .filter(n
-> !new File(dataPath
, n
).exists() && !new File(dataPath
, n
+ ".d").exists())
115 private File
getAccountsFile() {
116 return new File(dataPath
, "accounts.json");
119 private void createInitialAccounts() throws IOException
{
120 final var legacyAccountPaths
= getLegacyAccountPaths();
121 final var accountsStorage
= new AccountsStorage(legacyAccountPaths
.stream()
122 .map(number
-> new AccountsStorage
.Account(number
, number
, null))
125 IOUtils
.createPrivateDirectories(dataPath
);
126 var fileName
= getAccountsFile();
127 if (!fileName
.exists()) {
128 IOUtils
.createPrivateFile(fileName
);
131 final var pair
= openFileChannel(getAccountsFile());
132 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
133 saveAccountsLocked(fileChannel
, accountsStorage
);
137 private Set
<String
> getLegacyAccountPaths() {
138 final var files
= dataPath
.listFiles();
144 return Arrays
.stream(files
)
145 .filter(File
::isFile
)
147 .filter(file
-> PhoneNumberFormatter
.isValidNumber(file
, null))
148 .collect(Collectors
.toSet());
151 private List
<AccountsStorage
.Account
> readAccounts() {
153 final var pair
= openFileChannel(getAccountsFile());
154 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
155 return readAccountsLocked(fileChannel
).accounts();
157 } catch (IOException e
) {
158 logger
.error("Failed to read accounts list", e
);
163 private void updateAccounts(Function
<List
<AccountsStorage
.Account
>, List
<AccountsStorage
.Account
>> updater
) {
165 final var pair
= openFileChannel(getAccountsFile());
166 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
167 final var accountsStorage
= readAccountsLocked(fileChannel
);
168 final var newAccountsStorage
= updater
.apply(accountsStorage
.accounts());
169 saveAccountsLocked(fileChannel
, new AccountsStorage(newAccountsStorage
));
171 } catch (IOException e
) {
172 logger
.error("Failed to update accounts list", e
);
176 private AccountsStorage
readAccountsLocked(FileChannel fileChannel
) throws IOException
{
177 fileChannel
.position(0);
178 final var inputStream
= Channels
.newInputStream(fileChannel
);
179 return objectMapper
.readValue(inputStream
, AccountsStorage
.class);
182 private void saveAccountsLocked(FileChannel fileChannel
, AccountsStorage accountsStorage
) throws IOException
{
184 try (var output
= new ByteArrayOutputStream()) {
185 // Write to memory first to prevent corrupting the file in case of serialization errors
186 objectMapper
.writeValue(output
, accountsStorage
);
187 var input
= new ByteArrayInputStream(output
.toByteArray());
188 fileChannel
.position(0);
189 input
.transferTo(Channels
.newOutputStream(fileChannel
));
190 fileChannel
.truncate(fileChannel
.position());
191 fileChannel
.force(false);
193 } catch (Exception e
) {
194 logger
.error("Error saving accounts file: {}", e
.getMessage(), e
);
198 private static Pair
<FileChannel
, FileLock
> openFileChannel(File fileName
) throws IOException
{
199 var fileChannel
= new RandomAccessFile(fileName
, "rw").getChannel();
200 var lock
= fileChannel
.tryLock();
202 logger
.info("Config file is in use by another instance, waiting…");
203 lock
= fileChannel
.lock();
204 logger
.info("Config file lock acquired.");
206 return new Pair
<>(fileChannel
, lock
);