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