]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java
07dfac3c766ba95ef3af8ef69e817d74dcb5d564
[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 Set<String> getAllNumbers() {
45 return readAccounts().stream()
46 .map(AccountsStorage.Account::number)
47 .filter(Objects::nonNull)
48 .collect(Collectors.toSet());
49 }
50
51 public String getPathByNumber(String number) {
52 return readAccounts().stream()
53 .filter(a -> number.equals(a.number()))
54 .map(AccountsStorage.Account::path)
55 .findFirst()
56 .orElse(null);
57 }
58
59 public String getPathByAci(ACI aci) {
60 return readAccounts().stream()
61 .filter(a -> aci.toString().equals(a.uuid()))
62 .map(AccountsStorage.Account::path)
63 .findFirst()
64 .orElse(null);
65 }
66
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());
71 }
72
73 if (number != null && number.equals(a.number())) {
74 return new AccountsStorage.Account(a.path(), null, a.uuid());
75 }
76 if (aci != null && aci.toString().equals(a.toString())) {
77 return new AccountsStorage.Account(a.path(), a.number(), null);
78 }
79
80 return a;
81 }).toList());
82 }
83
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());
91 }
92 if (aci != null && aci.toString().equals(a.toString())) {
93 return new AccountsStorage.Account(a.path(), a.number(), null);
94 }
95
96 return a;
97 });
98 return Stream.concat(existingAccounts, Stream.of(account)).toList();
99 });
100 return accountPath;
101 }
102
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())
107 .findFirst()
108 .get();
109 }
110
111 private File getAccountsFile() {
112 return new File(dataPath, "accounts.json");
113 }
114
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))
119 .toList());
120
121 IOUtils.createPrivateDirectories(dataPath);
122 var fileName = getAccountsFile();
123 if (!fileName.exists()) {
124 IOUtils.createPrivateFile(fileName);
125 }
126
127 final var pair = openFileChannel(getAccountsFile());
128 try (final var fileChannel = pair.first(); final var lock = pair.second()) {
129 saveAccountsLocked(fileChannel, accountsStorage);
130 }
131 }
132
133 private Set<String> getLegacyAccountPaths() {
134 final var files = dataPath.listFiles();
135
136 if (files == null) {
137 return Set.of();
138 }
139
140 return Arrays.stream(files)
141 .filter(File::isFile)
142 .map(File::getName)
143 .filter(file -> PhoneNumberFormatter.isValidNumber(file, null))
144 .collect(Collectors.toSet());
145 }
146
147 private List<AccountsStorage.Account> readAccounts() {
148 try {
149 final var pair = openFileChannel(getAccountsFile());
150 try (final var fileChannel = pair.first(); final var lock = pair.second()) {
151 return readAccountsLocked(fileChannel).accounts();
152 }
153 } catch (IOException e) {
154 logger.error("Failed to read accounts list", e);
155 return List.of();
156 }
157 }
158
159 private void updateAccounts(Function<List<AccountsStorage.Account>, List<AccountsStorage.Account>> updater) {
160 try {
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));
166 }
167 } catch (IOException e) {
168 logger.error("Failed to update accounts list", e);
169 }
170 }
171
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);
176 }
177
178 private void saveAccountsLocked(FileChannel fileChannel, AccountsStorage accountsStorage) throws IOException {
179 try {
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);
188 }
189 } catch (Exception e) {
190 logger.error("Error saving accounts file: {}", e.getMessage(), e);
191 }
192 }
193
194 private static Pair<FileChannel, FileLock> openFileChannel(File fileName) throws IOException {
195 var fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
196 var lock = fileChannel.tryLock();
197 if (lock == null) {
198 logger.info("Config file is in use by another instance, waiting…");
199 lock = fileChannel.lock();
200 logger.info("Config file lock acquired.");
201 }
202 return new Pair<>(fileChannel, lock);
203 }
204 }