]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java
3c81c7d7294faa311a88d0cb89e9c6b0061677d9
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / accounts / AccountsStore.java
1 package org.asamk.signal.manager.storage.accounts;
2
3 import com.fasterxml.jackson.databind.ObjectMapper;
4
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;
12
13 import java.io.ByteArrayInputStream;
14 import java.io.ByteArrayOutputStream;
15 import java.io.File;
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;
25 import java.util.Set;
26 import java.util.function.Function;
27 import java.util.stream.Collectors;
28 import java.util.stream.Stream;
29
30 public class AccountsStore {
31
32 private final static Logger logger = LoggerFactory.getLogger(AccountsStore.class);
33 private final ObjectMapper objectMapper = Utils.createStorageObjectMapper();
34
35 private final File dataPath;
36
37 public AccountsStore(final File dataPath) throws IOException {
38 this.dataPath = dataPath;
39 if (!getAccountsFile().exists()) {
40 createInitialAccounts();
41 }
42 }
43
44 public synchronized Set<String> getAllNumbers() {
45 return readAccounts().stream()
46 .map(AccountsStorage.Account::number)
47 .filter(Objects::nonNull)
48 .collect(Collectors.toSet());
49 }
50
51 public synchronized Set<AccountsStorage.Account> getAllAccounts() {
52 return readAccounts().stream().filter(a -> a.number() != null).collect(Collectors.toSet());
53 }
54
55 public synchronized String getPathByNumber(String number) {
56 return readAccounts().stream()
57 .filter(a -> number.equals(a.number()))
58 .map(AccountsStorage.Account::path)
59 .findFirst()
60 .orElse(null);
61 }
62
63 public synchronized String getPathByAci(ACI aci) {
64 return readAccounts().stream()
65 .filter(a -> aci.toString().equals(a.uuid()))
66 .map(AccountsStorage.Account::path)
67 .findFirst()
68 .orElse(null);
69 }
70
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());
75 }
76
77 if (number != null && number.equals(a.number())) {
78 return new AccountsStorage.Account(a.path(), null, a.uuid());
79 }
80 if (aci != null && aci.toString().equals(a.toString())) {
81 return new AccountsStorage.Account(a.path(), a.number(), null);
82 }
83
84 return a;
85 }).toList());
86 }
87
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());
95 }
96 if (aci != null && aci.toString().equals(a.toString())) {
97 return new AccountsStorage.Account(a.path(), a.number(), null);
98 }
99
100 return a;
101 });
102 return Stream.concat(existingAccounts, Stream.of(account)).toList();
103 });
104 return accountPath;
105 }
106
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())
111 .findFirst()
112 .get();
113 }
114
115 private File getAccountsFile() {
116 return new File(dataPath, "accounts.json");
117 }
118
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))
123 .toList());
124
125 IOUtils.createPrivateDirectories(dataPath);
126 var fileName = getAccountsFile();
127 if (!fileName.exists()) {
128 IOUtils.createPrivateFile(fileName);
129 }
130
131 final var pair = openFileChannel(getAccountsFile());
132 try (final var fileChannel = pair.first(); final var lock = pair.second()) {
133 saveAccountsLocked(fileChannel, accountsStorage);
134 }
135 }
136
137 private Set<String> getLegacyAccountPaths() {
138 final var files = dataPath.listFiles();
139
140 if (files == null) {
141 return Set.of();
142 }
143
144 return Arrays.stream(files)
145 .filter(File::isFile)
146 .map(File::getName)
147 .filter(file -> PhoneNumberFormatter.isValidNumber(file, null))
148 .collect(Collectors.toSet());
149 }
150
151 private List<AccountsStorage.Account> readAccounts() {
152 try {
153 final var pair = openFileChannel(getAccountsFile());
154 try (final var fileChannel = pair.first(); final var lock = pair.second()) {
155 return readAccountsLocked(fileChannel).accounts();
156 }
157 } catch (IOException e) {
158 logger.error("Failed to read accounts list", e);
159 return List.of();
160 }
161 }
162
163 private void updateAccounts(Function<List<AccountsStorage.Account>, List<AccountsStorage.Account>> updater) {
164 try {
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));
170 }
171 } catch (IOException e) {
172 logger.error("Failed to update accounts list", e);
173 }
174 }
175
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);
180 }
181
182 private void saveAccountsLocked(FileChannel fileChannel, AccountsStorage accountsStorage) throws IOException {
183 try {
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);
192 }
193 } catch (Exception e) {
194 logger.error("Error saving accounts file: {}", e.getMessage(), e);
195 }
196 }
197
198 private static Pair<FileChannel, FileLock> openFileChannel(File fileName) throws IOException {
199 var fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
200 var lock = fileChannel.tryLock();
201 if (lock == null) {
202 logger.info("Config file is in use by another instance, waiting…");
203 lock = fileChannel.lock();
204 logger.info("Config file lock acquired.");
205 }
206 return new Pair<>(fileChannel, lock);
207 }
208 }