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 public void removeAccount(final String accountPath
) {
108 updateAccounts(accounts
-> accounts
.stream().filter(a
-> !a
.path().equals(accountPath
)).toList());
111 private String
generateNewAccountPath() {
112 return new Random().ints(100000, 1000000)
113 .mapToObj(String
::valueOf
)
114 .filter(n
-> !new File(dataPath
, n
).exists() && !new File(dataPath
, n
+ ".d").exists())
119 private File
getAccountsFile() {
120 return new File(dataPath
, "accounts.json");
123 private void createInitialAccounts() throws IOException
{
124 final var legacyAccountPaths
= getLegacyAccountPaths();
125 final var accountsStorage
= new AccountsStorage(legacyAccountPaths
.stream()
126 .map(number
-> new AccountsStorage
.Account(number
, number
, null))
129 IOUtils
.createPrivateDirectories(dataPath
);
130 var fileName
= getAccountsFile();
131 if (!fileName
.exists()) {
132 IOUtils
.createPrivateFile(fileName
);
135 final var pair
= openFileChannel(getAccountsFile());
136 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
137 saveAccountsLocked(fileChannel
, accountsStorage
);
141 private Set
<String
> getLegacyAccountPaths() {
142 final var files
= dataPath
.listFiles();
148 return Arrays
.stream(files
)
149 .filter(File
::isFile
)
151 .filter(file
-> PhoneNumberFormatter
.isValidNumber(file
, null))
152 .collect(Collectors
.toSet());
155 private List
<AccountsStorage
.Account
> readAccounts() {
157 final var pair
= openFileChannel(getAccountsFile());
158 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
159 return readAccountsLocked(fileChannel
).accounts();
161 } catch (IOException e
) {
162 logger
.error("Failed to read accounts list", e
);
167 private void updateAccounts(Function
<List
<AccountsStorage
.Account
>, List
<AccountsStorage
.Account
>> updater
) {
169 final var pair
= openFileChannel(getAccountsFile());
170 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
171 final var accountsStorage
= readAccountsLocked(fileChannel
);
172 final var newAccountsStorage
= updater
.apply(accountsStorage
.accounts());
173 saveAccountsLocked(fileChannel
, new AccountsStorage(newAccountsStorage
));
175 } catch (IOException e
) {
176 logger
.error("Failed to update accounts list", e
);
180 private AccountsStorage
readAccountsLocked(FileChannel fileChannel
) throws IOException
{
181 fileChannel
.position(0);
182 final var inputStream
= Channels
.newInputStream(fileChannel
);
183 return objectMapper
.readValue(inputStream
, AccountsStorage
.class);
186 private void saveAccountsLocked(FileChannel fileChannel
, AccountsStorage accountsStorage
) throws IOException
{
188 try (var output
= new ByteArrayOutputStream()) {
189 // Write to memory first to prevent corrupting the file in case of serialization errors
190 objectMapper
.writeValue(output
, accountsStorage
);
191 var input
= new ByteArrayInputStream(output
.toByteArray());
192 fileChannel
.position(0);
193 input
.transferTo(Channels
.newOutputStream(fileChannel
));
194 fileChannel
.truncate(fileChannel
.position());
195 fileChannel
.force(false);
197 } catch (Exception e
) {
198 logger
.error("Error saving accounts file: {}", e
.getMessage(), e
);
202 private static Pair
<FileChannel
, FileLock
> openFileChannel(File fileName
) throws IOException
{
203 var fileChannel
= new RandomAccessFile(fileName
, "rw").getChannel();
204 var lock
= fileChannel
.tryLock();
206 logger
.info("Config file is in use by another instance, waiting…");
207 lock
= fileChannel
.lock();
208 logger
.info("Config file lock acquired.");
210 return new Pair
<>(fileChannel
, lock
);