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 Set
<String
> getAllNumbers() {
45 return readAccounts().stream()
46 .map(AccountsStorage
.Account
::number
)
47 .filter(Objects
::nonNull
)
48 .collect(Collectors
.toSet());
51 public String
getPathByNumber(String number
) {
52 return readAccounts().stream()
53 .filter(a
-> number
.equals(a
.number()))
54 .map(AccountsStorage
.Account
::path
)
59 public String
getPathByAci(ACI aci
) {
60 return readAccounts().stream()
61 .filter(a
-> aci
.toString().equals(a
.uuid()))
62 .map(AccountsStorage
.Account
::path
)
67 public void updateAccount(String path
, String number
, ACI aci
) {
68 updateAccounts(accounts
-> accounts
.stream().map(a
-> {
69 if (path
.equals(a
.path())) {
70 return new AccountsStorage
.Account(a
.path(), number
, aci
== null ?
null : aci
.toString());
73 if (number
!= null && number
.equals(a
.number())) {
74 return new AccountsStorage
.Account(a
.path(), null, a
.uuid());
76 if (aci
!= null && aci
.toString().equals(a
.toString())) {
77 return new AccountsStorage
.Account(a
.path(), a
.number(), null);
84 public String
addAccount(String number
, ACI aci
) {
85 final var accountPath
= generateNewAccountPath();
86 final var account
= new AccountsStorage
.Account(accountPath
, number
, aci
== null ?
null : aci
.toString());
87 updateAccounts(accounts
-> {
88 final var existingAccounts
= accounts
.stream().map(a
-> {
89 if (number
!= null && number
.equals(a
.number())) {
90 return new AccountsStorage
.Account(a
.path(), null, a
.uuid());
92 if (aci
!= null && aci
.toString().equals(a
.toString())) {
93 return new AccountsStorage
.Account(a
.path(), a
.number(), null);
98 return Stream
.concat(existingAccounts
, Stream
.of(account
)).toList();
103 private String
generateNewAccountPath() {
104 return new Random().ints(100000, 1000000)
105 .mapToObj(String
::valueOf
)
106 .filter(n
-> !new File(dataPath
, n
).exists() && !new File(dataPath
, n
+ ".d").exists())
111 private File
getAccountsFile() {
112 return new File(dataPath
, "accounts.json");
115 private void createInitialAccounts() throws IOException
{
116 final var legacyAccountPaths
= getLegacyAccountPaths();
117 final var accountsStorage
= new AccountsStorage(legacyAccountPaths
.stream()
118 .map(number
-> new AccountsStorage
.Account(number
, number
, null))
121 IOUtils
.createPrivateDirectories(dataPath
);
122 var fileName
= getAccountsFile();
123 if (!fileName
.exists()) {
124 IOUtils
.createPrivateFile(fileName
);
127 final var pair
= openFileChannel(getAccountsFile());
128 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
129 saveAccountsLocked(fileChannel
, accountsStorage
);
133 private Set
<String
> getLegacyAccountPaths() {
134 final var files
= dataPath
.listFiles();
140 return Arrays
.stream(files
)
141 .filter(File
::isFile
)
143 .filter(file
-> PhoneNumberFormatter
.isValidNumber(file
, null))
144 .collect(Collectors
.toSet());
147 private List
<AccountsStorage
.Account
> readAccounts() {
149 final var pair
= openFileChannel(getAccountsFile());
150 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
151 return readAccountsLocked(fileChannel
).accounts();
153 } catch (IOException e
) {
154 logger
.error("Failed to read accounts list", e
);
159 private void updateAccounts(Function
<List
<AccountsStorage
.Account
>, List
<AccountsStorage
.Account
>> updater
) {
161 final var pair
= openFileChannel(getAccountsFile());
162 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
163 final var accountsStorage
= readAccountsLocked(fileChannel
);
164 final var newAccountsStorage
= updater
.apply(accountsStorage
.accounts());
165 saveAccountsLocked(fileChannel
, new AccountsStorage(newAccountsStorage
));
167 } catch (IOException e
) {
168 logger
.error("Failed to update accounts list", e
);
172 private AccountsStorage
readAccountsLocked(FileChannel fileChannel
) throws IOException
{
173 fileChannel
.position(0);
174 final var inputStream
= Channels
.newInputStream(fileChannel
);
175 return objectMapper
.readValue(inputStream
, AccountsStorage
.class);
178 private void saveAccountsLocked(FileChannel fileChannel
, AccountsStorage accountsStorage
) throws IOException
{
180 try (var output
= new ByteArrayOutputStream()) {
181 // Write to memory first to prevent corrupting the file in case of serialization errors
182 objectMapper
.writeValue(output
, accountsStorage
);
183 var input
= new ByteArrayInputStream(output
.toByteArray());
184 fileChannel
.position(0);
185 input
.transferTo(Channels
.newOutputStream(fileChannel
));
186 fileChannel
.truncate(fileChannel
.position());
187 fileChannel
.force(false);
189 } catch (Exception e
) {
190 logger
.error("Error saving accounts file: {}", e
.getMessage(), e
);
194 private static Pair
<FileChannel
, FileLock
> openFileChannel(File fileName
) throws IOException
{
195 var fileChannel
= new RandomAccessFile(fileName
, "rw").getChannel();
196 var lock
= fileChannel
.tryLock();
198 logger
.info("Config file is in use by another instance, waiting…");
199 lock
= fileChannel
.lock();
200 logger
.info("Config file lock acquired.");
202 return new Pair
<>(fileChannel
, lock
);