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