]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java
Store profile phone number sharing mode and discoverable state
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / RecipientHelper.java
1 package org.asamk.signal.manager.helper;
2
3 import org.asamk.signal.manager.api.RecipientIdentifier;
4 import org.asamk.signal.manager.api.UnregisteredRecipientException;
5 import org.asamk.signal.manager.api.UsernameLinkUrl;
6 import org.asamk.signal.manager.internal.SignalDependencies;
7 import org.asamk.signal.manager.storage.SignalAccount;
8 import org.asamk.signal.manager.storage.recipients.RecipientId;
9 import org.signal.libsignal.usernames.BaseUsernameException;
10 import org.signal.libsignal.usernames.Username;
11 import org.slf4j.Logger;
12 import org.slf4j.LoggerFactory;
13 import org.whispersystems.signalservice.api.push.ServiceId;
14 import org.whispersystems.signalservice.api.push.ServiceId.ACI;
15 import org.whispersystems.signalservice.api.push.ServiceId.PNI;
16 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
17 import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
18 import org.whispersystems.signalservice.api.services.CdsiV2Service;
19
20 import java.io.IOException;
21 import java.util.Collection;
22 import java.util.HashMap;
23 import java.util.HashSet;
24 import java.util.Map;
25 import java.util.Optional;
26 import java.util.Set;
27
28 import static org.asamk.signal.manager.config.ServiceConfig.MAXIMUM_ONE_OFF_REQUEST_SIZE;
29
30 public class RecipientHelper {
31
32 private static final Logger logger = LoggerFactory.getLogger(RecipientHelper.class);
33
34 private final SignalAccount account;
35 private final SignalDependencies dependencies;
36
37 public RecipientHelper(final Context context) {
38 this.account = context.getAccount();
39 this.dependencies = context.getDependencies();
40 }
41
42 public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) {
43 final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
44 if (address.number().isEmpty() || address.serviceId().isPresent()) {
45 return address.toSignalServiceAddress();
46 }
47
48 // Address in recipient store doesn't have a uuid, this shouldn't happen
49 // Try to retrieve the uuid from the server
50 final var number = address.number().get();
51 final ServiceId serviceId;
52 try {
53 serviceId = getRegisteredUserByNumber(number);
54 } catch (UnregisteredRecipientException e) {
55 logger.warn("Failed to get uuid for e164 number: {}", number);
56 // Return SignalServiceAddress with unknown UUID
57 return address.toSignalServiceAddress();
58 } catch (IOException e) {
59 logger.warn("Failed to get uuid for e164 number: {}", number, e);
60 // Return SignalServiceAddress with unknown UUID
61 return address.toSignalServiceAddress();
62 }
63 return account.getRecipientAddressResolver()
64 .resolveRecipientAddress(account.getRecipientResolver().resolveRecipient(serviceId))
65 .toSignalServiceAddress();
66 }
67
68 public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException {
69 final var recipientIds = new HashSet<RecipientId>(recipients.size());
70 for (var number : recipients) {
71 final var recipientId = resolveRecipient(number);
72 recipientIds.add(recipientId);
73 }
74 return recipientIds;
75 }
76
77 public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
78 if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
79 return account.getRecipientResolver().resolveRecipient(ACI.from(uuidRecipient.uuid()));
80 } else if (recipient instanceof RecipientIdentifier.Number numberRecipient) {
81 final var number = numberRecipient.number();
82 return account.getRecipientStore().resolveRecipientByNumber(number, () -> {
83 try {
84 return getRegisteredUserByNumber(number);
85 } catch (Exception e) {
86 return null;
87 }
88 });
89 } else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) {
90 var username = usernameRecipient.username();
91 return resolveRecipientByUsernameOrLink(username, false);
92 }
93 throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
94 }
95
96 public RecipientId resolveRecipientByUsernameOrLink(
97 String username, boolean forceRefresh
98 ) throws UnregisteredRecipientException {
99 final Username finalUsername;
100 try {
101 finalUsername = getUsernameFromUsernameOrLink(username);
102 } catch (IOException | BaseUsernameException e) {
103 throw new RuntimeException(e);
104 }
105 if (forceRefresh) {
106 try {
107 final var aci = dependencies.getAccountManager().getAciByUsername(finalUsername);
108 return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername());
109 } catch (IOException e) {
110 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
111 null,
112 null,
113 username));
114 }
115 }
116 return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> {
117 try {
118 return dependencies.getAccountManager().getAciByUsername(finalUsername);
119 } catch (Exception e) {
120 return null;
121 }
122 });
123 }
124
125 private Username getUsernameFromUsernameOrLink(String username) throws BaseUsernameException, IOException {
126 try {
127 final var usernameLinkUrl = UsernameLinkUrl.fromUri(username);
128 final var components = usernameLinkUrl.getComponents();
129 final var encryptedUsername = dependencies.getAccountManager()
130 .getEncryptedUsernameFromLinkServerId(components.getServerId());
131 final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername);
132
133 return Username.fromLink(link);
134 } catch (UsernameLinkUrl.InvalidUsernameLinkException e) {
135 return new Username(username);
136 }
137 }
138
139 public Optional<RecipientId> resolveRecipientOptional(final RecipientIdentifier.Single recipient) {
140 try {
141 return Optional.of(resolveRecipient(recipient));
142 } catch (UnregisteredRecipientException e) {
143 if (recipient instanceof RecipientIdentifier.Number r) {
144 return account.getRecipientStore().resolveRecipientByNumberOptional(r.number());
145 } else {
146 return Optional.empty();
147 }
148 }
149 }
150
151 public void refreshUsers() throws IOException {
152 getRegisteredUsers(account.getRecipientStore().getAllNumbers(), false);
153 }
154
155 public RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException, UnregisteredRecipientException {
156 final var address = resolveSignalServiceAddress(recipientId);
157 if (address.getNumber().isEmpty()) {
158 return recipientId;
159 }
160 final var number = address.getNumber().get();
161 final var serviceId = getRegisteredUserByNumber(number);
162 return account.getRecipientTrustedResolver()
163 .resolveRecipientTrusted(new SignalServiceAddress(serviceId, number));
164 }
165
166 public Map<String, RegisteredUser> getRegisteredUsers(
167 final Set<String> numbers
168 ) throws IOException {
169 if (numbers.size() > MAXIMUM_ONE_OFF_REQUEST_SIZE) {
170 final var allNumbers = new HashSet<>(account.getRecipientStore().getAllNumbers()) {{
171 addAll(numbers);
172 }};
173 return getRegisteredUsers(allNumbers, false);
174 } else {
175 return getRegisteredUsers(numbers, true);
176 }
177 }
178
179 private Map<String, RegisteredUser> getRegisteredUsers(
180 final Set<String> numbers, final boolean isPartialRefresh
181 ) throws IOException {
182 Map<String, RegisteredUser> registeredUsers = getRegisteredUsersV2(numbers, isPartialRefresh);
183
184 // Store numbers as recipients, so we have the number/uuid association
185 registeredUsers.forEach((number, u) -> account.getRecipientTrustedResolver()
186 .resolveRecipientTrusted(u.aci, u.pni, Optional.of(number)));
187
188 final var unregisteredUsers = new HashSet<>(numbers);
189 unregisteredUsers.removeAll(registeredUsers.keySet());
190 account.getRecipientStore().markUndiscoverablePossiblyUnregistered(unregisteredUsers);
191 account.getRecipientStore().markDiscoverable(registeredUsers.keySet());
192
193 return registeredUsers;
194 }
195
196 private ServiceId getRegisteredUserByNumber(final String number) throws IOException, UnregisteredRecipientException {
197 final Map<String, RegisteredUser> aciMap;
198 try {
199 aciMap = getRegisteredUsers(Set.of(number), true);
200 } catch (NumberFormatException e) {
201 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(number));
202 }
203 final var user = aciMap.get(number);
204 if (user == null) {
205 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(number));
206 }
207 return user.getServiceId();
208 }
209
210 private Map<String, RegisteredUser> getRegisteredUsersV2(
211 final Set<String> numbers, boolean isPartialRefresh
212 ) throws IOException {
213 final var previousNumbers = isPartialRefresh ? Set.<String>of() : account.getCdsiStore().getAllNumbers();
214 final var newNumbers = new HashSet<>(numbers) {{
215 removeAll(previousNumbers);
216 }};
217 if (newNumbers.isEmpty() && previousNumbers.isEmpty()) {
218 logger.debug("No new numbers to query.");
219 return Map.of();
220 }
221 logger.trace("Querying CDSI for {} new numbers ({} previous), isPartialRefresh={}",
222 newNumbers.size(),
223 previousNumbers.size(),
224 isPartialRefresh);
225 final var token = previousNumbers.isEmpty()
226 ? Optional.<byte[]>empty()
227 : Optional.ofNullable(account.getCdsiToken());
228
229 final CdsiV2Service.Response response;
230 try {
231 response = dependencies.getAccountManager()
232 .getRegisteredUsersWithCdsi(previousNumbers,
233 newNumbers,
234 account.getRecipientStore().getServiceIdToProfileKeyMap(),
235 token,
236 dependencies.getServiceEnvironmentConfig().cdsiMrenclave(),
237 null,
238 dependencies.getLibSignalNetwork(),
239 newToken -> {
240 if (isPartialRefresh) {
241 account.getCdsiStore().updateAfterPartialCdsQuery(newNumbers);
242 // Not storing newToken for partial refresh
243 } else {
244 final var fullNumbers = new HashSet<>(previousNumbers) {{
245 addAll(newNumbers);
246 }};
247 final var seenNumbers = new HashSet<>(numbers) {{
248 addAll(newNumbers);
249 }};
250 account.getCdsiStore().updateAfterFullCdsQuery(fullNumbers, seenNumbers);
251 account.setCdsiToken(newToken);
252 account.setLastRecipientsRefresh(System.currentTimeMillis());
253 }
254 });
255 } catch (CdsiInvalidTokenException e) {
256 account.setCdsiToken(null);
257 account.getCdsiStore().clearAll();
258 throw e;
259 } catch (NumberFormatException e) {
260 throw new IOException(e);
261 }
262 logger.debug("CDSI request successful, quota used by this request: {}", response.getQuotaUsedDebugOnly());
263
264 final var registeredUsers = new HashMap<String, RegisteredUser>();
265 response.getResults()
266 .forEach((key, value) -> registeredUsers.put(key,
267 new RegisteredUser(value.getAci(), Optional.of(value.getPni()))));
268 return registeredUsers;
269 }
270
271 public record RegisteredUser(Optional<ACI> aci, Optional<PNI> pni) {
272
273 public RegisteredUser {
274 aci = aci.isPresent() && aci.get().isUnknown() ? Optional.empty() : aci;
275 pni = pni.isPresent() && pni.get().isUnknown() ? Optional.empty() : pni;
276 if (aci.isEmpty() && pni.isEmpty()) {
277 throw new AssertionError("Must have either a ACI or PNI!");
278 }
279 }
280
281 public ServiceId getServiceId() {
282 return aci.map(a -> (ServiceId) a).or(this::pni).orElse(null);
283 }
284 }
285 }