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